Sign in to follow this  
RsblsbShawn

My Message Handling Functionality in AS

Recommended Posts

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?

Share this post


Link to post
Share on other sites
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.

Share this post


Link to post
Share on other sites
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);
}


Share this post


Link to post
Share on other sites
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

Share this post


Link to post
Share on other sites
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.

Share this post


Link to post
Share on other sites
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.

Share this post


Link to post
Share on other sites
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.

Share this post


Link to post
Share on other sites
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.

Share this post


Link to post
Share on other sites
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?

Share this post


Link to post
Share on other sites
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).

Share this post


Link to post
Share on other sites
Hmm.. that's unfortunate, because that's one of the primary features I was looking for in a scripting language =(...

At least AS works though, so it beats GM ;)

I wonder what I'd have to "donate" in order for that feature to get implemented =)

Share this post


Link to post
Share on other sites
Quote:
Original post by RsblsbShawn
Hmm.. that's unfortunate, because that's one of the primary features I was looking for in a scripting language =(...At least AS works though, so it beats GM ;)


You can probably still accomplish most of what you want by using basic design patterns. It may be require a few extra steps such as the use of interfaces and storing a behavior object in the class but is supported in the language. You might end up with something like:

class A : IEntity {
IEntityBehaviour @updateBEH;
A() { }
void update() {
if(updateBEH != null) {
updateBEH.exec(this);
}
}
}


Share this post


Link to post
Share on other sites
The following example shows how to implement a global factory for instantiation of objects across modules. This is the closest example I have that matches what you described in your original post (replacing an object at runtime). Hopefully this will help you on your journey a little...

#include "angelscript.h"
#include "utils.h"


static COutStream out;
static asIScriptStruct *objectFactory = NULL;



static const char *const scriptTest =
"IObject @object1; \n"
"IObject @object2; \n"
"IObject @object3; \n"
" \n"
"interface IObject1 \n"
"{ \n"
" void execute1(int id) const; \n"
" void execute2(int id); \n"
"} \n"
" \n"
"void DoTest(IObject @object) \n"
"{ \n"
" object.execute1(1); \n"
" object.execute2(2); \n"
"} \n"
" \n"
"void Test1() \n"
"{ \n"
" IObject @object; \n"
" \n"
" @object = objectFactory.create(); \n"
" DoTest(object); \n"
"} \n"
" \n"
"void Test2() \n"
"{ \n"
" DoTest(object1); \n"
" DoTest(object2); \n"
" DoTest(object3); \n"
"} \n"
" \n"
;



static const char *const scriptObject1 =
"class Object1 : IObject \n"
"{ \n"
" void execute1(int id) { execute(1, 1, id); } \n"
" void execute2(int id) { execute(1, 2, id); } \n"
"} \n"
" \n"
"class Object1Factory : IObjectFactory \n"
"{ \n"
" IObject @create() \n"
" { \n"
" execute(1, 0, 0); \n"
" return Object1(); \n"
" } \n"
"} \n"
;

static const char *const scriptObject2 =
"class Object2 : IObject \n"
"{ \n"
" void execute2(int id) { execute(2, 2, id); } \n"
" void execute1(int id) { execute(2, 1, id); } \n"
"} \n"
" \n"
"class Object2Factory : IObjectFactory \n"
"{ \n"
" IObject @create() \n"
" { \n"
" execute(2, 0, 0); \n"
" return Object2(); \n"
" } \n"
"} \n"
;




static void execute(asDWORD typeID, asDWORD methodID1, asDWORD methodID2)
{
if(0 == methodID1) {
printf("Creating type %d\n", typeID);
}
else {
printf("Object%d.execute%d() Object%d.execute%d()\n",
typeID,
methodID1,
typeID,
methodID2);
}
}




static void SetFactory(asIScriptEngine *engine,
const char *module,
const char *object)
{
int typeId = engine->GetTypeIdByDecl(module, object);
if(typeId >= 0) {
asIScriptStruct *obj;
obj = (asIScriptStruct *)engine->CreateScriptObject(typeId);
if(NULL != obj) {
if(NULL != objectFactory) {
objectFactory->Release();
}
objectFactory = obj;
}
}
}
bool TestXFactory()
{
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->RegisterGlobalFunction("void execute(int,int,int)", asFUNCTION(execute), asCALL_CDECL);
r = engine->RegisterInterface("IObject"); assert(r >= 0);
r = engine->RegisterInterfaceMethod("IObject", "void execute1(int)"); assert(r >= 0);
r = engine->RegisterInterfaceMethod("IObject", "void execute2(int)"); assert(r >= 0);
r = engine->RegisterInterface("IObjectFactory"); assert(r >= 0);
r = engine->RegisterInterfaceMethod("IObjectFactory", "IObject @create()"); assert(r >= 0);
r = engine->RegisterGlobalProperty("IObjectFactory @objectFactory", &(objectFactory = NULL)); assert(r >= 0);


// Build base test script
r = engine->AddScriptSection("test", NULL, scriptTest, strlen(scriptTest), 0); assert(r >= 0);
r = engine->Build("test"); assert(r >= 0);
r = engine->BindAllImportedFunctions("test"); assert(r >= 0);

// Build script 1
r = engine->AddScriptSection("object1", NULL, scriptObject1, strlen(scriptObject1), 0); assert(r >= 0);
r = engine->Build("object1"); assert(r >= 0);
r = engine->BindAllImportedFunctions("object1"); assert(r >= 0);

// Build script 2
r = engine->AddScriptSection("object2", NULL, scriptObject2, strlen(scriptObject2), 0); assert(r >= 0);
r = engine->Build("object2"); assert(r >= 0);
r = engine->BindAllImportedFunctions("object2"); assert(r >= 0);

// Object1: Set the factory, run the test, and allocate the first object
printf("Stage 1:\n");
SetFactory(engine, "object1", "Object1Factory@");
r = engine->ExecuteString("test", "Test1()"); assert(r >= 0);
r = engine->ExecuteString("test", "@object1 = @objectFactory.create()"); assert(r >= 0);

// Object2: Set the factory, run the test, and allocate the first object
printf("\nStage 2:\n");
SetFactory(engine, "object2", "Object2Factory");
r = engine->ExecuteString("test", "Test1()"); assert(r >= 0);
r = engine->ExecuteString("test", "@object2 = @objectFactory.create()"); assert(r >= 0);

// Object1: Set back to the first object type and create the last object.
printf("\nStage 3:\n");
SetFactory(engine, "object1", "Object1Factory");
r = engine->ExecuteString("test", "Test1()"); assert(r >= 0);
r = engine->ExecuteString("test", "@object3 = @objectFactory.create()"); assert(r >= 0);

// Test all objects
printf("\nStage 4:\n");
r = engine->ExecuteString("test", "DoTest(@object1)"); assert(r >= 0);
r = engine->ExecuteString("test", "DoTest(@object2)"); assert(r >= 0);
r = engine->ExecuteString("test", "DoTest(@object3)"); assert(r >= 0);


// If the factory was created release it
if(NULL != objectFactory) {
objectFactory->Release();
objectFactory = NULL;
}
engine->Release();

return (r < 0);
}

Share this post


Link to post
Share on other sites
Quote:
Original post by RsblsbShawn
I wonder what I'd have to "donate" in order for that feature to get implemented =)


I refuse to be "bribed" into implementing features that I don't think have a place in AngelScript. The only way I can be convinced to implement a feature is to show me that it will make AngelScript better for the majority.

However, I already have plans to make AngelScript more dynamic, it should for example be possible to "inject" new functions into an already compiled module. The intention is to allow modifiable scripts such as what you're describing. I can't say when I'll be able to implement this though.

Regards,
Andreas

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

Sign in to follow this