My Message Handling Functionality in AS

Started by
12 comments, last by WitchLord 16 years, 2 months ago
My engine is almost fully exposed to AS, but I can't quite figure out the best way to approach using my message handler. Here's how it works: I have a singleton class called "MessageHandler" defined as follows:

template <typename T>
class MessageHandlerClass : public MessageHandlerPublisher 
			  , private NoCopy, public Singleton<MessageHandlerClass> {
	public:
		void publish(const T *msg);
		void subscribe(MessageInterface<T> *l);
		void unsubscribe(const MessageInterface<T> *l);
	private:
		MessageHandlerClass();
		~MessageHandlerClass();
		std::vector<MessageInterface<T> *> listeners;
		std::queue<T *> messages;
};
(there's actually more things like delegates and delayed sending, but those aren't important for this discussion). Next there's the MessageInterface Interface/Class:

template <typename T>
struct MessageInterface {
	virtual ~MessageInterface() {
		MessageHandler<T>::Instance().unsubscribe(this);
	}
	virtual void receiveMessage(const T *msg) = 0;
};
Basically how this works is you can create any message whatsoever.. so for example a player input message like such:

struct PlayerIPMsg {
  PlayerIPMsg() { move_up = move_down = move_left = move_right = false; }
  bool move_up, move_down, move_left, move_right;
};
Then any class can listen for that message... so for example the player:

struct Player : public Entity, public MessageInterface<PlayerIPMsg> {
  Player() { MessageHandler<PlayerIPMsg>::Instance().subscribe(this); }
  void receiveMessage(const PlayerIPMsg *msg) {
    //handle movement here;
  }
};
This basically allows any data to pass between objects that have no idea about each other... I can't think of the best way to approach this in AS. I have about 25 base engine Message classes, not including any game-specific ones, so it'd be impractical for each IEntity to implement all message interfaces if it doesn't use them... Any ideas on how I should approach this?
Advertisement
I didn't realize interfaces supported MI! so never mind this =)

This sorta brings me to my next question tho:
Is it possible to register an object value as such: asOBJ_VALUE | asOBJ_POD | asOBJ_APP_CLASS_C

and still have a handle to it? I know as soon as you add asOBJ_REF to that it doesn't work, but is there a way to get that functionality?

Thanks so much!
-Shawn.
From what you described one approach would be to mirror local implementation requirements in the scripting environment. You can actually accomplish this with a single registered interface containing no methods (IListener). Each object which implements this interface can call one or more functions for registration. This eliminates any explicit requirements for implementation in scripts and allows for more dynamic event handling. Example code below (Modified for this situation):

//////////////////////////////////////////////////////////////////////////////////  Demonstrates using an interface with no methods for registering event//  receivers. This uses a single interface for simplicity and implements the//  following.////      *   System defined listener interface with no methods. This allows//          the interface to be user with no explicit implementation//          requirements.//      *   System defined player interface with implementation requirements.//      *   Two message types (MoveEvent and TalkEvent).//      *   Listener registration that determines which events the object//          receives by searching for specific declarations. Includes minimal//          caching of method id's.////  Supporting multiple interfaces and/or registration functions would require//  a more complex caching method. Although not demonstrated this approach can//  use more than one listener interface and can include implementation//  requirements.////////////////////////////////////////////////////////////////////////////////#include <assert.h>#include <map>#include "angelscript.h"#include "utils.h"struct CallbackData{    int onEvent_MoveEvent;    int onEvent_TalkEvent;    typedef std::map<int, CallbackData*> map;};static CallbackData::map    listeners;static COutStream           out;static const char *const    script ="class Player1 : IListener, IPlayer \n""{                                  \n""   Player1()                       \n""   {                               \n""       Listeners.Add(this);        \n""   }                               \n""                                   \n""   void die() {}                   \n""   void onEvent(MoveEvent @msg) {} \n""}                                  \n""                                   \n""class Player2 : IListener, IPlayer \n""{                                  \n""   Player2()                       \n""   {                               \n""       Listeners.Add(this);        \n""   }                               \n""                                   \n""   void die() {}                   \n""   void onEvent(MoveEvent @msg) {} \n""   void onEvent(TalkEvent @msg) {} \n""}                                  \n";static void DummyFunc(){}static void Add(asIScriptEngine *engine, asIScriptStruct *object){    assert(NULL != engine);    assert(NULL != object);    printf("Registering Listener\n");    int             typeId;    CallbackData::map::iterator iter;    CallbackData    *callbackData;    //  Grab the type ID of the     typeId = object->GetStructTypeId();    iter = listeners.find(typeId);    if(iter == listeners.end()) {        printf("Creating method id cache for object id %d\n", typeId);        //  FIXME: Memory leak - fix later        callbackData = new CallbackData;        memset(callbackData, 0, sizeof(*callbackData));        callbackData->onEvent_MoveEvent = engine->GetMethodIDByDecl(typeId, "void onEvent(MoveEvent @msg)");        callbackData->onEvent_TalkEvent = engine->GetMethodIDByDecl(typeId, "void onEvent(TalkEvent @msg)");        listeners[typeId] = callbackData;    }    else {        printf("Using method id cache for object id %08x\n", typeId);        callbackData = (*iter).second;    }    if(callbackData->onEvent_MoveEvent >= 0) {        printf("Object receives MoveEvent events\n");        /*        MessageHandler<MoveEvent>::Instance().subscribe(            MoveEvent_ScriptThunk(engine,            object,            callbackData->onEvent_MoveEvent));        */    }    if(callbackData->onEvent_TalkEvent  >= 0) {        printf("Object receives TalkEvent events\n");        /*        MessageHandler<TalkEvent>::Instance().subscribe(            TalkEvent_ScriptThunk(engine,            object,            callbackData->onEvent_TalkEvent));        */    }    //  This is a reference so let's release it!    object->Release();    printf("\n");    return;}bool TestListenerExample(){    asIScriptEngine     *engine;    int                 r;    engine = asCreateScriptEngine(ANGELSCRIPT_VERSION); assert(NULL != engine);    r = engine->SetMessageCallback(asMETHOD(COutStream,Callback), &out, asCALL_THISCALL); assert(r >= 0);    r = engine->RegisterInterface("IPlayer"); assert(r >= 0);    r = engine->RegisterInterfaceMethod("IPlayer", "void die()"); assert(r >= 0);    r = engine->RegisterInterface("IListener"); assert(r >= 0);    r = engine->RegisterObjectType("MoveEvent", 0, asOBJ_REF); assert(r >= 0);    r = engine->RegisterObjectBehaviour("MoveEvent", asBEHAVE_ADDREF, "void f()", asFUNCTION(DummyFunc), asCALL_GENERIC);    r = engine->RegisterObjectBehaviour("MoveEvent", asBEHAVE_RELEASE, "void f()", asFUNCTION(DummyFunc), asCALL_GENERIC);    r = engine->RegisterObjectType("TalkEvent", 0, asOBJ_REF); assert(r >= 0);    r = engine->RegisterObjectBehaviour("TalkEvent", asBEHAVE_ADDREF, "void f()", asFUNCTION(DummyFunc), asCALL_GENERIC);    r = engine->RegisterObjectBehaviour("TalkEvent", asBEHAVE_RELEASE, "void f()", asFUNCTION(DummyFunc), asCALL_GENERIC);    r = engine->RegisterObjectType("AngelScriptEngine", 0, asOBJ_REF | asOBJ_NOHANDLE); assert(r >= 0);    r = engine->RegisterObjectMethod("AngelScriptEngine", "void Add(IListener @player)", asFUNCTION(Add), asCALL_CDECL_OBJFIRST); assert(r >= 0);    r = engine->RegisterGlobalProperty("AngelScriptEngine Listeners", (void*)engine); assert(r >= 0);    if(r >= 0) {        r = engine->AddScriptSection(NULL, NULL, script, strlen(script), 0);    }    if(r >= 0) {        r = engine->Build(NULL);    }    engine->ExecuteString(NULL,        "Player1 player1;"        "Player2 player2;"        "Player1 player3;"        "Player2 player4;"        );    engine->Release();    return (r < 0);}
Quote:Original post by RsblsbShawn
I didn't realize interfaces supported MI! so never mind this =)

This sorta brings me to my next question tho:
Is it possible to register an object value as such: asOBJ_VALUE | asOBJ_POD | asOBJ_APP_CLASS_C

and still have a handle to it? I know as soon as you add asOBJ_REF to that it doesn't work, but is there a way to get that functionality?

Thanks so much!
-Shawn.


asOBJ_VALUE means that instances of the type can be allocated on the stack, which prohibits the use of handles. You can't keep references to an object allocated on the stack, since the object must be released when the stack is popped.

asOBJ_REF means that the instances are always allocated in dynamic memory (at least as far as AngelScript is concerned). This also means that instances of this type cannot be passed by value to application registered functions, though they can still be passed by value to script registered functions.

--

Thanks a lot to Digital_Asphyxia for his suggestion for solving the 'message handler' problem. It was a very good explanation, one that I may use myself in an upcoming tutorial.

Regards,
Andreas

AngelCode.com - game development and more - Reference DB - game developer references
AngelScript - free scripting library - BMFont - free bitmap font generator - Tower - free puzzle game

Ahhh... thanks so much Digital_Asphyxia, this is a great idea!

WitchLord: Let me see if I can phrase my question better, your answer answers exactly what I asked, I just don't think I explained myself well.

I have a type called "Colour" which its functionality should be obvious =).. I call things like: Colour c(1, 0, 0, 1); useColour(c); and it sets the current drawing colour to red.

I also have a ColourManager singleton that reads a bunch of colours from a file and lets me use them... so I can do something like:

ColourManager::Instance().getColour("Player Inside");
I abbreviate this to a macro like: COLOUR("Player Inside");

The fantastic thing about this, is my engine has colour slide bars. So I press the 9 key, and I can tab through all the registered colours, and open up rgba sliders and change the colour an watch all objects change on screen, then resave the colours. I can also create new colours on the fly just by calling COLOUR("new colour name"); and if it doesn't exist it adds it to the list automatically.

In order for this functionality to work, I need to have a pointer to the colour, as well as a traditional as value/POD type, and have the ability to switch between them at any time... is this possible?

Thanks again!
-Shawn.
Well, obviously you can't do it within the same registered type, it just wouldn't be safe in a scripted environment.

Instead I suggest you do it with two different types, ColourValue and ColourObject. ColourValue is obviously registered as asOBJ_VALUE, and ColourObject is registered as asOBJ_REF.

Then you can register a asBEHAVE_VALUE_CAST behaviour for ColourObject, that will allow AngelScript to implicitly convert it to a ColourValue where needed.

AngelCode.com - game development and more - Reference DB - game developer references
AngelScript - free scripting library - BMFont - free bitmap font generator - Tower - free puzzle game

Ahh.. asBEHAVE_VALUE_CAST.. perfect =) thanks!

I find AS to have fantastic documentation when you know what to look for... I promise I'm reading the docs and checking the tests before asking =)

I have one hopefully final question!

Once I load a script and call AddScriptSection on the engine for it, is there an easy way to unload it, and reload, or better yet to reload it without any fancy unloading?

The idea is I can edit the code while the game's running, so lets say I have a 10 object A's.. I change their behavior, the original 10 stay the same, but all new object A's that get made use new behavior.

This was possible with GameMonkey, hopefully AS can do it too... I'm doing some tests with modules, and just discarding the module and reloading it, but that's a big problem since you can import classes across modules.

Thanks again!
-Shawn.
Quote:Original post by RsblsbShawn
The idea is I can edit the code while the game's running, so lets say I have a 10 object A's.. I change their behavior, the original 10 stay the same, but all new object A's that get made use new behavior.

This was possible with GameMonkey, hopefully AS can do it too...


The short answer is no.

If you apply the same patterns to AS as any other object oriented language I'm sure a suitable solution can be obtained. First take into account that AS is statically typed so any new classes intended to replace existing ones must at the very least include the same method signatures as the original. This is necessary to ensure that existing code that uses the object call the right methods and the stack is not corrupted. Given the constraints and what you want to accomplish you can safely do this with a factory. This should allow you to do exactly what you described above. I have code that demonstrates how to do this.
If the binding is done just to a method name, then that's fine... for example:

load this:
class A : IEntity { A() {  registerEntity(this); //managed by C++ now } void update() {  print("hi!"); }}

call this function:
void f() { for (int i=0;i&lt;10;++i) {  A a;}

then change the script's update() to be: print("bye"); or something.

Is that at least possible as long as methods aren't destroyed?
Quote:Original post by RsblsbShawn
If the binding is done just to a method name, then that's fine... for example:

load this:
*** Source Snippet Removed ***
call this function:
*** Source Snippet Removed ***
then change the script's update() to be: print("bye"); or something.

Is that at least possible as long as methods aren't destroyed?



No, this is not possible at the API or script level. I believe Andreas is planning to add delegates at some point which would get you a bit closer to what you are trying to do (See the to do list).

This topic is closed to new replies.

Advertisement