[off-topic] event scheduling, timing, callbacks, etc.

futnuh

11-04-2007 08:12:08

Apologies in advance, the following is a little off-topic from the focus of python-ogre. (Rationalization: This is becoming less the case as python-ogre grows to encompass all aspects of game development.)

I'm curious how other people are implementing temporal events in their python-ogre programs? Things like countdown clocks, events that last a finite duration, etc. I've always liked the Twisted (and their python implementation of the Reactor pattern) but find myself wondering if this is the right approach for a non-networked application?

So how are you handling scheduling, and why did you make this decision?

Cheers,
Darran.

griminventions

11-04-2007 18:26:10

I wrote my own scheduler class. It's not difficult to do (thanks to Python) and it doesn't add any dependencies to my games.

I have a small CallbackEvent class that gets put onto a stack of scheduled callbacks, sorted by time (soonest on top). Each game tick, the scheduler looks at the top item and if the current time is larger or equal to the event's time, it fires. It keeps looking at the next top item until something won't fire, then it exits.

Simple, but effective for my needs. I can post the source if you're interested. It's not that big.

futnuh

11-04-2007 21:19:29

Yes, please - if you don't mind sharing I'd appreciate it. No use reinventing the proverbial wheel ...

griminventions

12-04-2007 00:16:31

This is part of an engine I'm developing based on PTK (for 2d) and Python-Ogre (for 3d). It will be used for prototyping game concepts, and hopefully for some full releases if I can convince publishers that Python is a Good Thing. :) I'll be setting up a website for it etc in the near future.

My main loop updates this each tick, and I use generators for the update function because they are way faster than a plain function call (since they retain state there's no setup before each call). So what you do is assign the generator to smoe variable and then call next() on it to update the scheduler.

Ie:

schedMan = ScheduleManager()

# later, at your main loop...
scheduler = schedMan.getUpdater() # create the generator

while ( quit is False ):
scheduler.next() # process all events


Sorry for the sparse example, but hopefully you can adapt the code easily.


__author__ = "Jason McIntosh, www.creatrixgames.com"

#===============================================================================
# The ScheduleManager schedules callbacks. It's simple and intended for a mostly
# single purpose of being able to defer callbacks.
# This is a class also for states can each have a scheduler that will pause
# when the state is paused.
#===============================================================================

import globals
import coretiming
import logging

logger = logging.getLogger( "ScheduleManager" )
logger.addHandler( globals.logHandler )
logger.setLevel( globals.logLevel )


#-------------------------------------------------------------------------------
# CallbackEvent
#-------------------------------------------------------------------------------
class CallbackEvent( object ):
__slots__ = { "callback":None, "wait":0, "timestamp":0, "args":None, "recurring":False }
def __init__( self, function, wait, recurring, *args ):
self.args = args
self.callback = function
self.wait = wait
self.recurring = recurring


#-------------------------------------------------------------------------------
# ScheduleManager class
#-------------------------------------------------------------------------------
class ScheduleManager( object ):
def __init__( self ):
self.callbacks = []
self.newCallbacks = [] # new events to add next update
self.cancelCallbacks = [] # events to cancel next update
logger.debug( "Instance created" )

def __del__( self ):
print "ScheduleManager deleted"

def schedule( self, function, wait, recurring=False, *args ):
"""Add a callback to the schedule queue with a specific time delta from
now. Callbacks are inserted in sorted order based on time. This way, the
next one to be fired is always the first in the list. Returns the
CallbackEvent for reference (ie, to call cancel() if needed).
"""
callbackEvent = CallbackEvent( function, wait, recurring, *args )
self.newCallbacks.append( callbackEvent )
logger.debug( "created new CallbackEvent id=" + str(id(callbackEvent)) + " " + str(function) )
return callbackEvent

def cancel( self, callbackEvent ):
"""Canceled callbacks are removed during the manager's update.
"""
self.cancelCallbacks.append( callbackEvent )

def getUpdater( self ):
"""Insert newly scheduled callback events, remove canceled callbacks,
and execute any callbacks that are scheduled for now.
"""
timer = coretiming.instance
while True:
# NEW CALLBACK EVENTS ----------------------------------------------
for newCallback in self.newCallbacks:
newCallback.timestamp = timer.now + newCallback.wait
added = False
for i, ce in enumerate( self.callbacks ):
if newCallback.wait < ce.wait: # smaller times go first
added = True
# insert new CallbackEvent before this CallbackEvent
self.callbacks.insert( i, newCallback )
break
# otherwise, insert new CallbackEvent at end of list
if added is False:
self.callbacks.append( newCallback )
if len(self.newCallbacks) > 0:
self.newCallbacks = []

# CANCEL CALLBACK EVENTS -------------------------------------------
for ce in self.cancelCallbacks:
try:
self.callbacks.remove( ce )
except:
pass
# logger.debug( "Could not CANCEL CALLBACK: id=" + str(id(ce)) + " " + str(ce.callback) )
if len( self.cancelCallbacks ) > 0:
self.cancelCallbacks = []

# EXECUTE CALLBACK EVENTS ------------------------------------------
while len( self.callbacks ) > 0:
ce = self.callbacks[0]
if ce.timestamp <= timer.now:
ce.callback( *ce.args )
del self.callbacks[0]
if ce.recurring is True:
ce.timestamp = timer.now + ce.wait
self.newCallbacks.append( ce )
else:
break
yield None


#-------------------------------------------------------------------------------
# Global schedule manager
instance = ScheduleManager()

Tubez

12-04-2007 09:49:49

I have an event class that's basically a time (when to execute) a function name (what to call), parameters to said function (args and kwargs) and some housekeeping information (who is posting the event, for whom is it intended if anyone in particular). Then I arrange all events in a heapq for cheap sorting. Take note of heapq, its a lifesaver if you have many deferred events.
You need to define __cmp__ for heapq to work properly though.

Once every loop-through my event manager checks if the first event in the queue is up for dispatching. If so, it gets the destination object's handler (by prepending "event_" and using getattr to retrieve the target object's callable, so the event handlers are in a "separate" namespace) and invokes it with the specified args and kwargs.

It's very simple but very flexible.

futnuh

16-04-2007 08:32:49

Thanks guys for the code and advice. (heapq is *very* nice.)

griminventions

16-04-2007 08:39:16

heapq does look nice. I implemented my own heapq, I guess, but if Python's is in C, I might switch if it gives me any performance gains. Thanks for the pointer.