Game Development Community

EventManager not calling subscribers... [solved with workaround]

by Max Kielland · in Torque Game Builder · 02/23/2013 (10:37 am) · 18 replies

Well it was to good to be true. I my previous post I got one subscriber to work, but when I added one more to the same event I ran into something strange.

The first subscriber is always called but never the second one. UNLESS! I single step (Torsion) through the code, now all subscribers get called.


#1
02/24/2013 (11:10 am)
new ScriptObject(Listener) { class = TestClass; };   
new EventManager(GameEventManager) { queue = "EventQueue";} ;   
GameEventManager.registerEvent("ModuleUpdate");   
GameEventManager.subscribe(Listener,"ModuleUpdate");   
GameEventManager.dumpEvents();   
GameEventManager.dumpSubscribers();   
GameEventManager.postEvent("ModuleUpdate","Data");   
  
function TestClass::onModuleUpdate(%this,%data) {   
  
  echo(%this SPC %data);     
}

Your subscription is wrong. It's (listener object, event name, listener class method)

function initializeAIEventManager()
{
    if (!isObject(AIEventManager))
    {
        $AIEventManager = new EventManager(AIEventManager)
        { 
            queue = "AIEventQue"; 
        };
        
        // Module related signals
        AIEventManager.registerEvent("_UnitUnderAttack");
    }
    
    if (!isObject(AIListener))
    {
        $AIListener = new ScriptMsgListener(AIListener) 
        { 
            class = "AIEventListener"; 
        };
        
        // Module related subscriptions
        AIEventManager.subscribe(AIListener, "_UnitUnderAttack", "unitUnderAttack");
    }
}

function AIEventListener::unitUnderAttack(%this, %messageData)
{
    // do stuff here
}

To do this with any SimObject (using the above event manager), you do this:
function mySimObject::initialize(%this)
{
    AIEventManager.subscribe(mySimObject, "_UnitUnderAttack", "unitUnderAttack");
}

function mySimObject::unitUnderAttack(%this, %messageData)
{
    // do what I do when I get this message
}
#2
02/24/2013 (11:20 am)
So, your script should resemble this:
new ScriptObject(Listener) { class = TestClass; };      
new EventManager(GameEventManager) { queue = "EventQueue";} ;      
GameEventManager.registerEvent("ModuleUpdate");

// Notice the difference here:  
GameEventManager.subscribe(TestClass, "ModuleUpdate", "onModuleUpdate");
// All objects of class TestClass will use TestClass::onModuleUpdate to handle this event

GameEventManager.dumpEvents();      
GameEventManager.dumpSubscribers();      
GameEventManager.postEvent("ModuleUpdate","Data");      
     
function TestClass::onModuleUpdate(%this,%data) {      
     
  echo(%this SPC %data);        
}
#3
02/25/2013 (1:50 pm)
Thank you for your reply.

In the script reference it says that if you don't state a callback name, the EventManager will put "on" in front of the event name as the callback name.

So if I register "MyEvent" and subscribe without a callback specified, then "onMyEvent" will be called.

It is working if I single step through the code or if I put an echo("test") at the end of the first subscribers callback.

Anyhow, I will try your suggestion to see if it makes any difference.

[EDIT]
Ahh, you mean that the listener is the CLASS name not an instance of a class?
#4
02/25/2013 (2:38 pm)
Nope, there was no difference. Only the first subscriber gets called, unless I single step through the callback onInstallModule() where the next subscribers onInstallModule() gets called immediately.

Only if I put an echo("Subscriber 1") at the end of the first subscriber's onInstallModule, the second subscriber gets called. It seems to be some sort of timing issue in the EventManager.
#5
02/25/2013 (3:51 pm)
That's really weird. The code I posted above is from my AI tutorial that I haven't posted yet (T3D) and works perfectly.

A quick look at the source confirms that it does indeed tack "on" to the beginning of that event name, but I'm skeptical of that stuff in general and prefer to name it myself.

Here's where I subscribe (and unsubscribe) AI units to listen for other units being attacked:
/// <summary>
/// This function requests that the AIClientManager spawn a unit and add it to its group.
/// </summary>
/// <param name="name">The desired unit name - this is the SimName of the object and must be unique or "".</param>
/// <param name="spawnLocation">The position or object (spawnpoint, path object) to spawn the unit at.</param>
/// <param name="datablock">The datablock that the unit should use.</param>
/// <param name="priority">The priority of this unit. 0 to 2 from low to high priority.  Defaults to 1.</param>
/// <param name="onPath">If spawnLocation is a path, this should be true to get the unit to spawn on and follow the path.</param>
/// <return>Returns the new unit.</return>
function AIClientManager::addUnit(%this, %name, %spawnLocation, %datablock, %priority, %onPath)
{
	if (%this.client $= "")
	{
		echo(" !!!! AIClientManager is not assigned to a client - cannot add unit");
		return 0;
	}
    %newUnit = AIManager.addUnit(%name, %spawnLocation, %datablock, %priority, %onPath);
    %newUnit.team = (%this.client !$= "" ? %this.client : 0);
    %newUnit.AIClientMan = %this;
    AIEventManager.subscribe(%newUnit, "_UnitUnderAttack", "unitUnderAttack");
    %this.unitList.add(%newUnit);
    return %newUnit;
}

function AIClientManager::removeUnit(%this, %unit)
{
    if (%this.unitList.isMember(%unit))
        %this.unitList.remove(%unit);
    %unit.AIClientMan = "";
    %index = %this.messageQue.getCount() - 1;
    while(%index >= 0)
    {
        %msg = %this.messageQue.getObject(%index);
        %sender = getField(%msg.message, 0);
        if (%sender == %unit)
        {
            %this.messageQue.remove(%msg);
            %msg.delete();
        }
        %index--;
    }
}

If you create a new project, then check out https://github.com/RichardRanft/AITutorial.git and merge the scripts into the project you can see it in action. All AI units share the same callback:
/// <summary>
/// This function handles the unitUnderAttack event.  It determines if there is
/// a datablock-specific response to the event and calls it, or calls the default
/// AIPlayer response.
/// The format of <msgData> is assumed to be <originatingUnit>TAB<messageHandler>TAB<tab-delimited data>
/// </summary>
/// <param name="msgData">Data to pass to the actual event handler.
function AIPlayer::unitUnderAttack(%this, %msgData)
{
    %unit = getField(%msgData, 0);
    if (%this.team != %unit.team || %this.respondedTo == %unit)
        return;
    %method  = getField(%msgData, 1);
    %datablock = %this.getDataBlock();
    if (%datablock.isMethod(%method))
        eval(%datablock.getName()@"."@%method@"(%this, \""@%msgData@"\");");
    else if (%this.isMethod(%method))
    {
        eval("%this."@%method@"(\""@%msgData@"\");");
    }
}
which checks to see if this unit has already responded to this cry for help, then checks the unit's datablock and calls a datablock-specific method if it exists or an object specific method if that exists. Spawning a bunch of friendlies and then dropping an enemy nearby is like kicking a hornets' nest.
#6
02/25/2013 (3:57 pm)
Wait - are you trying to post an event when a module loads? This might be causing timing issues, which would be why stepping through script allows the other subscribers to catch the posted event. The engine will load all modules while you are stepping through script.

Oh, and you subscribe SimObjects as listeners, but the class field of the ScriptEventListener object is a special namespace that will also catch the event if the method exists there. Technically I don't think you need a ScriptEventListener object - you should be able to just create the EventManager and then let SimObjects subscribe to what they want.
#7
02/25/2013 (5:39 pm)
I think I need to explain myself a bit more. It seems like we are talking about different type of subscribers.

This is the full story:

In my game the user can "install" and "uninstall" modules in a factory. Then he can manipulate these modules depending on what they represent.

A "module" here is nothing more than a class derived from a ScriptObject.
There are one class for each type of module in the game.

It got a bit messy with calls all over the place when the modules interacted so I figured to implement an event system. Then I found the EventManager class and it seemed like it was doing exactly what I wanted.

So my thought is:
Whenever a module is "installed" in the factory an "ModuleInstall" event will be posted.

A subscriber here is the HUD, the ModuleWindow, and some other classes that need to react on this "ModuleInstall" event. All these are of different classes but mainly based on ScriptObject.

In my simple test I have a t2dTileLayer instance (Factory) and a guiWindowCtrl (ModuleWindow).

So what I mean with a subscriber is a single instance, not the individual instances of a class.

First I registered an event with GameEventManager.registerEvent("ModuleInstall").
Then I have my Factory::onModuleInstall(%this,%data) and ModuleWindow:onModuleInstall(%this,%data) both have called the GameEventManager.subscribe(ModuleWindow,"ModuleInstall") resp. GameEventManager.subscribe(Factory,"ModuleInstall")... in that order.

When I call GameEventManager.postEvent("ModuleInstall") the first subscriber ModuleWindow::onModuleInstall() is always called, but not Factory::onModuleInstall() unless I single step through ModuleWindow::onModuleInstall().

I don't know if I have misunderstood what a subscriber is, I interpreted it as many different instances, where you can call GameEventManager.subscribe() many times to add an arbitrary number of instances to get called on the event.

If you only can subscribe one class to have all its instances called, then I guess this isn't what I'm looking for.

Maybee I just need an explanation of how the EventManager is supposed to work...
#8
02/26/2013 (12:25 am)
What you're describing sounds like it should work, but try this:

Make a single module manager object and have it subscribe as the sole listener for the onModuleInstall event, then have it take appropriate action based on the data passed in the event message.

Or, perhaps have the module schedule the postEvent() call 100 ms or so after it has loaded.

Any SimObject-based object can subscribe to an event - including behavior instances. The examples I have used all work fine with up to 500 AI Player objects subscribed to the event shown and all of them reacting appropriately. This is why I'm having trouble seeing where this is going wrong for you; I've used event managers often and I've never seen them behave that way.
#9
02/26/2013 (10:38 pm)
Now I'm really confused.

I created a new empty project with one Scene object to trigger the whole process (trigger).

new EventManager(MyEventManager) { Queue = "MyEventQueue"; };

new ScriptObject(ObjectA_1) { class = TestObjectA; };
new ScriptObject(ObjectA_2) { class = TestObjectA; };
new ScriptObject(ObjectA_3) { class = TestObjectA; };
new ScriptObject(ObjectB_1) { class = TestObjectB; };
new ScriptObject(ObjectB_2) { class = TestObjectB; };
new ScriptObject(ObjectB_3) { class = TestObjectB; };

function TestObjectA::onEvent(%this,%data) {

  echo(%this SPC %data);  
}

function TestObjectB::onEvent(%this,%data) {

  echo(%this SPC %data);  
}

function trigger::onLevelLoaded(%this) {

  MyEventManager.registerEvent("MyEvent");
  MyEventManager.subscribe(ObjectA_1,"MyEvent","onEvent");
  MyEventManager.subscribe(ObjectA_2,"MyEvent","onEvent");
  MyEventManager.subscribe(ObjectA_3,"MyEvent","onEvent");
  MyEventManager.subscribe(ObjectB_1,"MyEvent","onEvent");
  MyEventManager.subscribe(ObjectB_2,"MyEvent","onEvent");
  MyEventManager.subscribe(ObjectB_3,"MyEvent","onEvent");
  MyEventManager.postEvent("MyEvent","was called by MyEvent");
}

All the objects are called correctly!
#10
02/27/2013 (1:02 am)
Seems like you have found a really interesting and unique issue... lol!

Well, hopefully this will get you on the track to getting your module system functioning.
#11
02/27/2013 (8:59 am)
My faulty setup do involve a superclass gui control, maybe there is a glitch in that specific scenario.

I will try to isolate the problem, thank you for your input.
#12
02/27/2013 (1:11 pm)
I nailed it down to a very specific and strange situation. It turned out that the ModuleWindow always broke the chain of subscribe callbacks when it executed:

%CallbackClass = %this.Class;

This broke the chain, but if I replaced it with...

%CallbackClass = %this.getFieldValue("Class");

..it worked perfectly well, and all subscribers got called. I guess I have found one of thosw strange bugs that rarely comes up to the surface.

It is still a mystery why, but the workaround fixed my issues... for now... :P
#13
02/28/2013 (11:54 pm)
I'm lost here - where and how is %CallbackClass being used? I'm guessing that the reason I've never seen this misbehavior is because I'm not doing whatever it is you're doing here.... lol
#14
03/01/2013 (8:53 am)
I have optimized away the %CallbackClass but I then got the same problem at another place in a sort routine, sorting a SimSet.

It looks like the subscription chain breaks as soon I assign a value to an non existing variable like.

%this.newVariable = "something";

I don't know the mechanism for how the EventManager is calling the subscribers (is it in a thread? A protected state? etc..). I decided to abandon the EventManger and write my own as I had planned from the beginning.
#15
03/01/2013 (2:52 pm)
I have optimized away the %CallbackClass but I then got the same problem at another place in a sort routine, sorting a SimSet.

It looks like the subscription chain breaks as soon I assign a value to an non existing variable like.

%this.newVariable = "something";

I don't know the mechanism for how the EventManager is calling the subscribers (is it in a thread? A protected state? etc..). I decided to abandon the EventManger and write my own as I had planned from the beginning.
#16
03/02/2013 (7:16 am)
I'm still not understanding how this is breaking. Where are you setting the %this.newVariable = "something";? in the TestClass method? Or in each listener's method?

Here is my current test case in T2D:
// Builds the event manager and script listener that will be responsible for
// handling important Shell system events.
function initializeShellEventManager()
{
    if (!isObject(ShellEventManager))
    {
        $ShellEventManager = new EventManager(ShellEventManager)
        { 
            queue = "ShellEventQueue"; 
        };
        
        // Module related signals
        ShellEventManager.registerEvent("_ModuleLoaded");
    }
    
    if (!isObject(ShellListener))
    {
        $ShellListener = new ScriptMsgListener(ShellListener) 
        { 
            class = "ShellBase"; 
        };
        
        // Module related subscriptions
        ShellEventManager.subscribe(ShellListener, "_ModuleLoaded", "onModuleLoaded");
    }
}

// Cleanup the event manager
function destroyShellEventManager()
{
    if (isObject(ShellEventManager) && isObject(ShellListener))
    {
        // Remove all the subscriptions
        ShellEventManager.remove(ShellListener, "_ModuleLoaded");
        
        // Delete the actual objects
        ShellEventManager.delete();
        ShellListener.delete();
        
        // Clear the global variables, just in case
        $ShellEventManager = "";
        $ShellListener = "";
    }
}

function addListeners()
{
	new ScriptObject(Listener1);
	new ScriptObject(Listener2);
	new ScriptObject(Listener3);
	ShellEventManager.subscribe(Listener1, "_ModuleLoaded", "onModuleLoaded");
	ShellEventManager.subscribe(Listener2, "_ModuleLoaded", "onModuleLoaded");
	ShellEventManager.subscribe(Listener3, "_ModuleLoaded", "onModuleLoaded");
}

function Listener1::onModuleLoaded(%this, %data)
{
	%this.messageData = %data;
	echo(" @@@ Listener1::onModuleLoaded : " @ %this.messageData);
}

function Listener2::onModuleLoaded(%this, %data)
{
	%this.messageData = %data;
	echo(" @@@ Listener2::onModuleLoaded : " @ %this.messageData);
}

function Listener3::onModuleLoaded(%this, %data)
{
	%this.messageData = %data;
	echo(" @@@ Listener3::onModuleLoaded : " @ %this.messageData);
}

I added this to all modules:
ShellEventManager.postEvent("_ModuleLoaded", "BookReader::create()");
but replace "BookReader" with the module name for each.

Output is as follows:

@@@ Listener1::onModuleLoaded : TestScene::create()
@@@ Listener2::onModuleLoaded : TestScene::create()
@@@ Listener3::onModuleLoaded : TestScene::create()
@@@ Listener1::onModuleLoaded : BookReader::create()
@@@ Listener2::onModuleLoaded : BookReader::create()
@@@ Listener3::onModuleLoaded : BookReader::create()

echoing the previously undefined field on %this from each object.

I'm having great difficulty reproducing this issue....
#17
03/12/2013 (4:51 pm)
Sorry for not replying, have been busy elsewhere for a while...

I had a look in the source and figured that it was using some OS stuff to create a thread safe queue (if I got it right?).

I wrote my own EventManager in SC with the same function names but without a queue. Instead all subscribers are called instantly by the postEvent() function.

This gives me better control of when and in what order things are called. My EventManager works precisely as I want it to :)

However, the problem I saw was very strange indeed and when I get some spare time I will see if I can recreate the problem with a smaller example. If I can't I can send you my project (need to find and pull out the right version from SVN)...
#18
03/14/2013 (5:36 am)
No worries - you can see how I was using it by looking in the AI Tutorial scripts in my repo:

https://github.com/RichardRanft/AITutorial

Specifically, scripts/server/aiEventManager.cs, scripts/server/aiClientManager.cs and scripts/server/aiPlayer.cs (specifically AIPlayer::unitUnderAttack() which dispatches the message based on the unit's datablock).

If I were to roll my own event manager I think I might add a "priority" mechanism, but the way the current system works is otherwise fine by me (except for the weird bug you seem to have found).