Ok,
I got most concepts of the language but I still need to understand which is the best way to solve my use scenario (which will be applied as a solution to many classes).
A premise: current code is not working because ScriptableObjectNode::createSprite
doesn't get called.
Basically the situation is the following: I have a class ObjectNode
which is rendering a Object*
instance, each ObjectNode
is built by a factory pattern through a std::function<ObjectNode*(const Object*)>
, there is a specialized factory function for each possible type of Object
(which is stored as a const ObjectSpec*
inside Object
instance
class ObjectNode
{
protected:
const Object* object;
// a lot of common utility methods
void createSprite(rect rect, point position);
public:
ObjectNode(const Object* object) : object(object) { }
virtual void generate() = 0;
virtual void update(float dt) = 0;
};
class NativeObjectNode : public ObjectNode
{
public:
void generate() override { createSprite(...); }
};
class ObjectRenderer
{
ObjectNode* build(const Object* object) { .. }
void registerBuilder(const ObjectSpec* spec, std::function<ObjectNode*(const Object*)> builder) { .. }
};
This above is the native implementation which I'm currently using and that I'd like to open to be scriptable (by overriding generate
and update
methods.
I saw this: https://www.angelcode.com/angelscript/sdk/docs/manual/doc_adv_inheritappclass.html in the manual but this has more requirements than me, since I don't want to be able to instantiate these classes in the script, I just want the ability to override methods by having access to other methods of the class itself.
So I tried to figure out the most efficient and elegant way to do this, first I defined a scriptable ObjectNode
by delegating the interface methods to the script object, then I added a custom wrapper function for createSprite
to ease parameter passing:
class ScriptableObjectNode : public ObjectNode
{
private:
asIScriptObject* scriptObject;
asIScriptContext* context;
asIScriptFunction *scriptGenerate, *scriptUpdate;
public:
ScriptableObjectNode(const Object* object, asIScriptContext* context, asIScriptFunction* generate, asIScriptFunction* update) :
ObjectNode
(object),
context(context), scriptGenerate(generate), scriptUpdate(update), scriptObject(nullptr)
{
}
~ScriptableObjectNode()
{
if (scriptObject)
scriptObject->Release();
}
ScriptableObjectNode& opAssign(const ScriptableObjectNode& other)
{
assert(false);
return *this;
}
void setScriptObject(asIScriptObject* scriptObject)
{
this->scriptObject = scriptObject;
scriptObject->AddRef();
}
void createSprite(float x, float y, float w, float h, float px, float py)
{
ObjectNode
::createSprite(rect(x, y, w, h), point(px, py));
}
void generate() override
{
context->Prepare(scriptGenerate);
context->SetObject(scriptObject);
context->Execute();
}
void update(float dt) override
{
context->Prepare(scriptUpdate);
context->SetObject(scriptObject);
context->SetArgFloat(0, dt);
context->Execute();
}
};
Then I defined a script interface to provide proper design to custom nodes and I registered a class which is supposed to be ScriptableObjectNode
itself visible from the script:
openNameSpace("gfx::objects");
registerInterface("ObjectGfxNode");
registerInterfaceMethod("ObjectGfxNode", "void generate()");
registerInterfaceMethod("ObjectGfxNode", "void update(float dt)");
registerMethod("SuperObjectGfxNode", "void createSprite(float rx, float ry, float rw, float wh, float x, float y)", CALL_METHOD(modding::as::classes::ObjectNode, createSprite));
registerMethod("SuperObjectGfxNode", "SuperObjectGfxNode& opAssign(const SuperObjectGfxNode& in)", CALL_METHOD(modding::as::classes::ObjectNode, opAssign));
Then I defined a middleware AbstractObjectGfxNode
script class to keep track of the native instance which is actually holding the object itself (but the native class will always have the ownership):
abstract class AbstractObjectGfxNode : gfx::objects::ObjectGfxNode
{
gfx::objects::SuperObjectGfxNode@ self;
void setSelf(gfx::objects::SuperObjectGfxNode@ self) final { this.self = self; }
void generate() { }
void update(float dt) { }
}
class MyCustomNode : AbstractObjectGfxNode
{
void generate()
{
self.createSprite(920, 1040, 48, 80, 0, 0);
}
void update(float dt)
{
}
};
so at this point I have a function which register a new factory for a specific const ObjectSpec*
:
static bool registerObjectRenderer(const std::string& object, const std::string& className)
{
asIScriptModule* module = engine->GetModule("");
asITypeInfo* type = module->GetTypeInfoByDecl(className.c_str());
assert(type);
asIScriptFunction* factory = type->GetFactoryByDecl(fmt::format("{} @{}()", className, className).c_str());
assert(factory);
asIScriptFunction* generateMethod = type->GetMethodByDecl("void generate()");
asIScriptFunction* updateMethod = type->GetMethodByDecl("void update(float dt)");
asIScriptFunction* setSelfMethod = type->GetMethodByDecl("void setSelf(gfx::objects::SuperObjectGfxNode@ self)");
const ObjectSpec* spec = Data::d().object(object);
std::function<ObjectNode*(const Object*)> builder = [=](const Object* object)
{
auto* objectNode = new ScriptableObjectNode(object, context, generateMethod, updateMethod);
context->Prepare(factory);
context->Execute();
asIScriptObject* scriptObject = *reinterpret_cast<asIScriptObject**>(context->GetAddressOfReturnValue());
objectNode->setScriptObject(scriptObject);
int r = context->Prepare(setSelfMethod); assert(r >= 0);
r = context->SetObject(scriptObject); assert(r >= 0);
context->SetArgObject(0, objectNode);
context->Execute();
return objectNode;
};
objectRenderer->registerBuilder(spec, builder);
return true;
}
This is the best design I came up with, since this will possibly be applied to many different classes and my experience with AngelScript is limited to roughly 3 days, before starting to prepare all the necessary glue code I'd like to know if I missed any simpler solution than this.
Thanks,
I hope the code is enough clear!