Advertisement

Providing script environment

Started by November 27, 2023 07:30 AM
6 comments, last by Jackozzo 11 months, 3 weeks ago

Hi!

I'm implementing a modding system for my game and I'm searching the more efficient way to provide references to C++ backend classes to mods.

My current implementation defines a ModInterface interface programmatically which is then used by single mod entities:

class MyMod : ModInterface {
  void onDataLoaded(Game@ game) {
    ..
  }
}

Then each mod is compiled in its own module. This works but requires to pass the environment to all the methods of the mod each time, some methods could require more arguments, eg: Game@ game, GameData@ data, GfxData@ gfxData which are all pointers to C++ objects which have a lot of methods registered in AngelScript engine.

Now I'd like to avoid all this and provide global accessors to these classes through RegisterGlobalProperty but it doesn't look like to work. I guess because instance methods are not global functions so they don't have access to the global properties? I'm trying something really simple as

engine->RegisterGlobalProperty("const Game@ game", &game);

Of course any best practice related to what I'm trying to achieve is well accepted.
Thanks

Please explain the problem you're having with more detail. Are you getting any compiler errors? Or is it run-time errors? If so what are they?

Registering global properties like you're doing is perfectly fine, and they will be visible to all the scripts and reachable from within script class methods just like any other global variables declared in the script.

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

It's a compilation error related to the fact that the variable is not found, my code is the following;

check(engine->RegisterGlobalProperty("const Game@ testGame", &game));

Then in the script I do the following:

void onDataLoaded(Game@ game)
{
  testGame.seed();
  ...

Don't mind the actual Game@ game passed which is the one currently working (which has the downside of being passed each time I call the method instead that just setting it once in the engine like I'd like to do).

When I try to compile the script I get:

 payload.ls (35, 5) : Error : No matching symbol 'testGame'

The module is build by a CScriptBuilder instance which correctly finds all the other types registered by the engine.

That is odd indeed. It should work.

As I don't see your code I can only make a few guesses for you to look into:

  • Is check() a macro similar to assert() that is removed by pre-processor in release mode?
  • What is RegisterGlobalProperty returning?
  • Are you calling SetDefaultNamespace() with any value other than the default global namespace?
  • Are you calling RegisterGlobalProperty before building the script?

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

Thank you for you quick reply. The issue was indeed the namespace which wasn't the default one (I'm trying to organize all the API I'm exposing).

I'm still dubious regarding some design aspects: to avoid using static references to the engine or context: I have a native method which registers a builder able to instantiate an AngelScript class and forward some native calls to the AS instance. This is done by the script itself to register some scripted classes as target of game logic.

I do this by class name, eg:

static bool registerObjectRenderer(const std::string& object, const std::string& className)
{
  const ObjectSpec* spec = Data::d().object(object);

  class ScriptedObjectNodeFactory : public ::gfx::ObjectNodeFactory
  {
  private:
    asIScriptFunction* factoryMethod;
    asIScriptFunction* generateMethod;
    asIScriptFunction* updateMethod;
    asIScriptFunction* setSelfMethod;

  public:
    ScriptedObjectNodeFactory(const std::string& className, asITypeInfo* type)
    {
      generateMethod = type->GetMethodByDecl("void generate()");
      updateMethod = type->GetMethodByDecl("void update(float dt)");
      setSelfMethod = type->GetMethodByDecl("void setSelf(gfx::objects::SuperObjectGfxNode@ self)");
      factoryMethod = type->GetFactoryByDecl(fmt::format("{} @{}()", className, className).c_str());
    }

    ::gfx::ObjectNode* build(const Object* object) const override
    {
      auto* objectNode = new modding::as::classes::ObjectNode(object, ASE::context, generateMethod, updateMethod);

      ASE::context->Prepare(factoryMethod);
      ASE::context->Execute();
      asIScriptObject* scriptObject = *reinterpret_cast<asIScriptObject**>(ASE::context->GetAddressOfReturnValue());
      objectNode->setScriptObject(scriptObject);

      int r = ASE::context->Prepare(setSelfMethod); assert(r >= 0);
      r = ASE::context->SetObject(scriptObject); assert(r >= 0);
      r = ASE::context->SetArgObject(0, objectNode); assert(r >= 0);
      r = ASE::context->Execute(); assert(r >= 0);

      return objectNode;
    }
  };

  asITypeInfo* type = ASE::module->GetTypeInfoByDecl(className.c_str());
  assert(type);

  ::gfx::Gfx::data.registerObjectRenderer(spec, new ScriptedObjectNodeFactory(className, type));

  return true;
}

Now, this works but require a static reference to the context and to the module containing the definition (in the code stored in ASE::context and ASE::module).

So I wonder which is the best practice here to avoid these references, the method is registered in the engine through

registerFunction("bool registerObjectRenderer(const string& in object, const string& in className)", CALL_FUNCTION(gfx::registerObjectRenderer), "register a class as renderer of a specific object, class must inherit ObjectGfxNode interface");

I assume this function will always be called from a script?

If so you can

  1. get the active script context with a call to asGetActiveContext.
  2. You can then use that context to get the script function that makes the call with asIScriptContext::GetFunction.
  3. Then use asIScriptFunction::GetModule to get the module in which the function was compiled.
  4. From the asIScriptContext you can also get the engine with GetEngine and then request a context from a pool of context, create a new one, or, you can use the same context and just nest a another call on it with PushState.

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

Thank you for your clear explaination. I refactored the code to improve the structure. I still have some questions but I'll open another thread in case just to not go too much off-topic.

This topic is closed to new replies.

Advertisement