Jump to content
  • Advertisement
Sign in to follow this  
BUnzaga

JavaScript EventManager 4 U

This topic is 3340 days old which is more than the 365 day threshold we allow for new replies. Please post a new topic.

If you intended to correct an error in the post then please contact us.

Recommended Posts

I have been working on this for a bit now, I am a fan of collaboration, so I figured I would release it out into the universe and see if anyone could use it, or make it better, etc. The engine I use (DX Studio) uses javascript as its scripting language, so this is basically a javascript EventManager. It also incorporates a singleton approach, so there is only ever one EventManager in memory at any given time. My hopes is that people can use this if they need an event manager in javascript, or perhaps see how I did this, and make their own Event Manager in their code of choice. Here is the actual code:
//special thanks to DX Studio forum user Pat for helping me better
//understand these concepts and posting his WIP event system.

/*
	v10112009		
	BUnzaga
*/

EventManager._em = null;
function EventManager(){
	
	//if we haven't already created an EventManager, create one
	if(EventManager._em == null){
		EventManager._em = this;
	}
	//if we already have an EventManager, return that event manager.
	else{
		return EventManager._em;
	}
	
	//map of event types
	var listeners = {};

	//helper function for Array object, more accurate than
	function isNull(element, index, array){
		return element == null;
	}
	
	/*
		Registers a listener for an Object. Pass in the Object,
		a string name for the event to listen for and the callback function.
	*/
	this.registerListener = function(obj, e, callback){
		if(!(e in listeners)){
		    //create a new array of objects that are listening for e.
			listeners[e] = [];
		}
		
		//see if we are already listening for this object/event/callback
		var j=listeners[e].length;
		var i = 0;
		for(; i < j; i++){
			if(listeners[e]['obj'] === obj && listeners[e]['callback'] === callback){
				return;
			}
		}
		//add the object/callback to the event
		listeners[e].push({obj:obj, callback:callback});
	}
	
	/*
		Unregisters a listener for an Object. Pass in the Object and
		the event string name it was listening for. If there are no more Objects tied
		to the event, the event listener will be deleted.  You can also optionally pass in the
		callback function, if there is more than one callback listening for this event.
	*/
	this.unregisterListener = function(obj, e, callback){
		if(e in listeners){
		    //if there was no callback function passed in
			if(callback == null){
				for(var i = listeners[e].length-1; i > -1; i--){
					if(listeners[e]['obj'] === obj){
						listeners[e].splice(i, 1);
					}
				}
			}
			else{
			    //if there was a callback function passed in
				for(var i = listeners[e].length-1; i > -1; i--){
					if(listeners[e]['obj'] === obj && listeners[e]['callback'] === callback){
						listeners[e] = null;
						listeners[e].splice(i, 1);
					}
				}
			}
			//if there are no more objects listening for this event type
			//delete the event from the listeners map
			if(listeners[e].every(isNull)){
				listeners[e] = null;
				delete listeners[e];
			}
		}
	}
	
	/*
		Raises an event. Pass in the event string, and an optional 'data' object
		this could be an object, an array, a value, just about anything.
	*/
	this.raiseEvent = function(e, data){
	    //if we are listening for this event
		if(e in listeners){
			for(var i = (listeners[e].length-1);i > -1; i--){
			    //call all the callback functions listening for the event, passing the data
			    //to be used in the callback function
				listeners[e]['callback'].call(listeners[e]['obj'], data);
			}
		}
	}
}


Here is an example of it in a web html page: Samoti Games http://www.samotigames.com/tests/BUnzaga_EM_Test.html I don't claim to be a javascript guru, so I hope anyone who knows how to make this faster or better would post their findings. A couple caveats: I made the option to pass in the callback function, for a couple reasons. First, sometimes the same object might want to listen for the same event type, but for different callbacks. Such an example, perhaps you want to listen for moving the mouse on the X axis and separately listen for the Y axis. This way you can register to listen for mouseMove independent of each other. Secondly, sometimes the event doesn't explicitly match the callback function name. I ran into this when I was loading files, and raising an event with the filename as part of the event, but the callback function was the same for every file loader object. IE: fileName+"_loded" would be the event, and 'onLoadComplete' was the function name. Overall I like this separation, and it hasn't seemed to impede me in any way, in fact it has opened more opportunities. Some examples of how this works: 1. Create an instance of the EventManager: var em = new EventManager(); 2. Register objects to listen for events in the event manager. You can either do this using the 'this' object, or by passing in an object. //assuming 'mouseMove' is a function inside the 'this' object. em.registerListener(this, "mouseMove", mouseMove); //assuming 'mousTracker' is an object somewhere with a //public 'mouseMove' function. em.registerListener(mouseTracker, "mouseMove", mouseTracker.mouseMove); 3. Raise events. //assuming 'rawDX' and 'rawDY' contain the change in value of mouseX and mouseY. em.raiseEvent("mouseMove", {dx:rawDX, dy:rawDY}); As you can see, we are passing in a javascript object with the fields 'dx' and 'dy'. 4. In our callback function, we can use the optional data passed in. //this is the callback function in the 'mouseTracker' object. this.mouseMove = function(data){ alert("("+data.dx+", "+data.dy+")"); } 5. You can unregister listeners, even inside the callback function. You can either unregister each explicit callback function, or unregister all callback functions for that event type, for that object. //unregisters all 'mouseMove' listeners for 'this' object. em.unregisterListener(this, "mouseMove"); //unregisters the 'mouseMove' listener for only the function 'yAxisMove' em.unregisterListener(this, "mouseMove", yAxisMove); //unregisters the 'mouseMove' listener for the 'mouseMove' function, from //within the 'mouseMove' callback. this.mouseMove = function(data){ alert("("+data.dx+", "+data.dy+")"); em.unregisterListener(this, "mouseMove", this.mouseMove); } I tried to make it as dynamic as possible, and cover most situations.

Share this post


Link to post
Share on other sites
Advertisement
Quote:
Original post by BUnzaga
I don't claim to be a javascript guru, so I hope anyone who knows how to make this faster or better would post their findings.
Telling us which browser it works in would be useful; nothing happens in Opera 10, Internet Explorer 8 or Firefox 3 (well, IE 8 gets the little script error warning triangle).

What advantages would your manager give us over existing ones such as the one in MooTools?

Share this post


Link to post
Share on other sites
Oh dang! That is embarrassing.

It works great with google chrome. I had to rearrange some of the script, now it also is working in IE. Not sure about Mozilla, the boxes stay empty.

My main point wasn't to say my system is better than another system. In fact, it is probably far inferior. I don't do a lot of HTML coding, and I have never heard of MooTools. I was just hoping that someone starting out might get inspired or be able to see a working example and get their own ideas.

I personally had to do a lot of research, picking pieces of code from various languages and examples to construct this, I even posted a few questions on these forums about it.

It has worked wonders for the projects I have worked on, and has brought my programming experience to a new level. I am a big fan of event driven programming now, and just wanted to pass the enthusiasm on.

Sorry if it sounded like I was coming off cocky or like my system was the 'end all' system.

Share this post


Link to post
Share on other sites
Quote:
Original post by BUnzaga
It works great with google chrome. I had to rearrange some of the script, now it also is working in IE. Not sure about Mozilla, the boxes stay empty.
It's working in Opera 10 too, now. [smile]

Quote:
My main point wasn't to say my system is better than another system. In fact, it is probably far inferior. I don't do a lot of HTML coding, and I have never heard of MooTools. I was just hoping that someone starting out might get inspired or be able to see a working example and get their own ideas.
It's certainly a valuable learning experience, especially if you find the results useful in your own project - sorry if I came across as harsh!

Share this post


Link to post
Share on other sites
Ow. Ow. Ow. Where to begin... ?

Quote:
EventManager._em = null;
function EventManager(){
First, you're assigning a member to a function before defining that function, which is invariably going to hurt you (assigning a property to something that is not defined yet). If anything, you should have written:
function EventManager(){}
EventManager._em = null;


Not that it matters, because what you implement here is a Monostate (a singleton would be achieved by making the constructor private). Besides, making the manager a singleton is much easier than conditionals in constructors:

var EventManager = (function(){
var self = function(){...};
return new self();
})();


Quote:
if(EventManager._em == null)
This is equivalent to if(EventManager._em).

Quote:

//helper function for Array object, more accurate than
function isNull(element, index, array){
return element == null;
}
if(isNull(foo,bar,qux)) is equivalent to if(foo) here, so why the function?

Quote:
this.registerListener = function(obj, e, callback){
Theoretically speaking, shouldn't you be adding functions to the prototype to improve readability? Also be careful of users who want to register non-member functions... maybe make the object argument optional by placing it in last position.
Quote:
		var j=listeners[e].length;
var i = 0;
for(; i < j; i++){
Sounds like a job for bicycle repair man for-in : for(var i in listeners[e]).

Besides, you could chain the registration function by returning this:
self.prototype.register = function(event, callback, obj){
var func = { obj : obj, callback : callback };
if (event in this.listeners) {
for (var i in this.listeners[event])
if (this.listeners[event].callback === callback &&
this.listeners[event].obj === obj )
return;
this.listeners[event].push(func);
} else {
this.listeners[event] = [func];
}
return this;
};


Quote:
		if(e in listeners){
Since the entire function body is contained within this conditional, you might want to use the linus trick (early-return if the condition is false).

Quote:
				for(var i = listeners[e].length-1; i > -1; i--){
if(listeners[e]['obj'] === obj){
listeners[e].splice(i, 1);
}
}
You've reinvented array filtering.

Quote:
if(listeners[e].every(isNull)){
Shouldn't this test be a simple if(!listeners[e].length) ?

Quote:
listeners[e] = null;
delete listeners[e];
Why are you deleting null?

I would probably rewrite this as:

self.prototype.unregister(event,obj,callback){
if (!(event in this.listeners)) return this;

this.listeners[event] = this.listeners[event].filter(
callback
? function(f){return f.obj !== obj || f.callback !== callback;}
: function(f){return f.obj !== obj;}
);
this.listeners = this.listeners.filter(
function(a){return a.length > 0;}
);
return this;
};


Quote:
/* Pass in the event string, and an optional 'data' object this could be an object, an array, a value, just about anything. */
Why not just pass an arbitrary number of arguments?

self.prototype.run = function(event)
{
if (!(event in this.listeners)) return this;
var handlers = this.listeners[event];
return function() {
for (var i in handlers) {
handlers.callback.apply(handlers.obj, arguments);
}
};
};


On the other hand, I would rather have multiple instances of this system, since it lets me handle different behaviors in different parts of my code through dependency injection. The code amounts to:

[source type="javascript"]signals = (function(){
s = function() { this._c = s; };
s.prototype.channel = function(c) {
var h = [],
s = function() { for (var k in h) if (h[k]) h[k].apply(this,arguments); };
s.bind = function(f) { h.push(f); return h.length-1; };
s.unbind = function(f) { h[k] = null; };
return this.set(c,s);
};
s.prototype.set = function(n,v){
var i = function(){ this._c = i; };
i.prototype = new this._c();
i.prototype[n] = v;
return new i();
};
return s;
})();



From here.

Share this post


Link to post
Share on other sites
WOW. Just WOW.

Thank you for taking the time to post all this great information!

Most of what you see is what I have been able to hack and paste together using google and trial and error.

I'll be making a lot of modifications, and implementing a lot of what you suggested.

Share this post


Link to post
Share on other sites
Hmm, you could probably use a reintroduction to javascript.

While I'm linking to Mozilla, I should probably state that MDC may possibly be home to the internet's best reference documentation for javascript, both for the language and web DOM. They even document deprecated IE features.

Share this post


Link to post
Share on other sites
I haven't had a chance to work on this until tonight, but here is what I ended up with after taking a look at your suggestions:

var em = (function(){
var self = function(){
private:
listeners = {};
handlers = [];
}
// register listener you can pass in null, event, callback if you
// just want to run a callback function with no object
self.prototype.registerListener = function(o, e, c){
if(e in listeners){
for(var i in listeners[e]){
if(listeners[e].callback === c && listeners[e].obj === o){
return this;
}
}
listeners[e][listeners[e].length]={obj:o, callback:c};
}
else{
listeners[e] = [{obj:o, callback:c}];
}
return this;
}
// unregister pass in object (or null), event, and callback. Leave the callback null
// if you want to remove all callbacks for this event.
self.prototype.unregisterListener = function(o, e, c){
if(!(e in listeners)){return this;}
if(c){
listeners[e] = listeners[e].filter(function(f){return f.obj!==o || f.callback!==c;});
}
else{
listeners[e] = listeners[e].filter(function(f){return f.obj!==o;});
}
if(listeners[e].length<1){delete listeners[e];}
return this;
}
// raise event, pass in event type and optional 'arguments'
self.prototype.raise = function(e){
if(!(e in listeners)){
return this;
}
handlers = listeners[e];
for(var i = 0, j; j=handlers;i++){
handlers.callback.apply(handlers.obj, arguments);
}
return this;
}
// debug utility
self.prototype.toString = function(){
var response = "<EventManager: \n listeners:";
for(var i in listeners){
response +="\n "+i;
for(var j = 0, k=listeners.length;j<k;j++){
response +="\n "+listeners[j].callback.name;
}
}
return response+"\n>";
}
return new self();
})();



For my purposes, I like passing in the object first, then it also coincides with the 'call' and 'apply' functions. If I don't want to tie it to an object, I can pass in null.

I also opted to make this a singleton as you were nice to show me an example of how to do it correctly. For my purposes, I will only need one of these globally, and if I need another one for a separate object, I can include it for that objects script and it will allow me to only create one per-object.

If I do find a reason why I will need more, I can just modify a few lines and now I can create as many 'new' ones as I need.

I wanted to make sure I knew what was going on with your array filter command, so I broke it up into two functions, just so I could wrap my head around it.

Anyway, thanks again for the help. I am very happy with it thus far. :)

Share this post


Link to post
Share on other sites
Sign in to follow this  

  • Advertisement
×

Important Information

By using GameDev.net, you agree to our community Guidelines, Terms of Use, and Privacy Policy.

GameDev.net is your game development community. Create an account for your GameDev Portfolio and participate in the largest developer community in the games industry.

Sign me up!