Advertisement

Interoperation between native and script class

Started by April 26, 2022 07:37 PM
2 comments, last by Jackozzo 2 years, 7 months ago

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!

This design is very similar to how I do it myself in my own home-brewed game engine that I use for prototyping. You'll also find something similar in the game sample that comes with the AngelScript SDK.

I can't think of a simpler solution ? I would love to hear it if anyone else can.

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

Advertisement

That's what I wanted to know ?

Thanks for you quick responses, I promise I'll stop asking things as soon as I have a good understanding of everything. It's just that I don't want to find myself in a situation in which I have to refactor a lot of code for a poor design choice.

I was wondering why this code is not working though, I can see that setSelf is called, and even generate, which should call self.createSprite but the call is not forwared to the native code. I guess it's something really trivial.

NVM, I found the issue, I was setting self reference through this.self = self but I had to do @this.self = self ?

This topic is closed to new replies.

Advertisement