🎉 Celebrating 25 Years of GameDev.net! 🎉

Not many can claim 25 years on the Internet! Join us in celebrating this milestone. Learn more about our history, and thank you for being a part of our community!

Assigning C++ object to the script object in less explicit way

Started by
10 comments, last by WitchLord 6 years, 2 months ago

Hello,

I'm following similar approach as is demonstrated in Angelscript's "game" sample - so I have a C++ object that provides some very low level functionality (positioning, rendering, physics). These are all registered with Angelscript and available for that C++ object, lets call it GameObject. Then I want to achieve something very similar to what happens in sample game - I have various script-side objects that all refer to instance of this C++ side object. I just don't want to do this in such explicit way, where the object is expected to be passed in constructor and is then provided upon instantiating script object on C++ side (game/source/scriptmgr.cpp:230). This has one drawback - it requires the class to always provide this constructor that takes GameObject@ as a single argument.

What I wanted initially, was to make a script-side object that will be a wrapper around C++ level functionality, so it won't require a convention of calling C++ methods like this: `self.someMethod` but will provide a wrapper that can be then inherited from, allowing us to completely hide this bridge between AS and C++:


shared abstract class ScriptObject: IController
{
     ScriptObject(GameObject@ obj) 
     {
         @self = obj;
     }

     void render()
     {
         self.render();
     }

     void move()
     {
         self.move();
     }
}


class Npc: ScriptObject
{
  Npc() 
  {
    # setup npc 
  }
}

class Monster: Npc 
{
  Monster()
  {
    # setup monster
  }
}

Above is some pseudocode of what I mean - notice how child classes do not define constructors that take GameObject - this is my main problem with the AS's game implementation, because it requires EVERY class to provide this constructor, and having many objects this is a lot of code that should be transparent to the user, and provided "by default". 

So the main question of this post is: IS IT DOABLE? And if so, how can it be achieved? I was thinking about requiring only empty constructor, and then providing "self" by a setter on base class (ScriptObject) through IController interface or something like this, but maybe there is a better way? Has anyone tried something similar? I think it could be great if we had a way to hide this bridge between C++ and script and make the base script class a bit more transparent.

PS. (EDIT) Of course, rubber duck programming had to kick in, once I explained it and looked at official docs, I found another method which does not take the C++ object when constructing script object, but reverses the situation - it creates C++ object itself in a constructor, so it's always guaranteed to be constructed (http://www.angelcode.com/angelscript/sdk/docs/manual/doc_adv_inheritappclass.html). It's interesting solution and I need to check if this does what I need fully, but if anyone has other ideas how to make the whole process more automated and less reliable on implementing it in every possible class in hierarchy - please share them!


Where are we and when are we and who are we?
How many people in how many places at how many times?
Advertisement

Looks like the second solution does what I'd like (hides creation and passing C++ instance to the script obj), but makes one thing that is also important to me a bit harder. My plan was to instantiate objects based on filename, not class name - so each object has it's own filename like torch.as, sword.as, troll.as and these can only be created by designated function like spawnObject(scriptfile), in a very similar way like it happens in mentioned game sample - there, it spawns the first class that implements IController interface that was found in a provided script file. This is something I'd like to keep, because I want to refer to files rather than classes.

Unfortunately the second solution does not allow script object instantiation to be controlled fully by C++ side and permits instantiation from within the scripts, bypassing any "guards". Is there some way to use second method (so C++ object is created inside script object constructor) and also control how script object will be instantiated? In game sample, the script object requires CGameObj which can only be created from C++ side, so it naturally blocks instantiation from within scripts other than through "spawn" method. In that other method I see no way to achieve similar behaviour.

I hope this makes any sense, let me know if this requires better explanation so I will draw some diagrams and more code :) These are my needs:

  • script object requires C++ proxy object to provide full functionality 
  • script object should hide most part of its C++ dependency under its implementation :
    • wrapper methods around methods provided from C++
    • no direct access to C++ proxy object (only through ^ wrapper methods)
    • no need to pass C++ proxy object in a constructor of every game object
  • script object and all derived classes can't be instantiated by normal means of the language, only through special factory function that takes script file as argument, ie. spawnObject("obj/weapons/sword.as")

 


Where are we and when are we and who are we?
How many people in how many places at how many times?

Anything is possible :) it's just a matter of figuring out the steps to get there. 

Building on what you have already, I can think of one solution to your problem where you would have the constructor of ScriptObject call a specific registered function to obtain the instance of the C++ object, let's call it GetCPPInstance for now. GetCPPInstance can then have the logic to throw an exception if the condition under which it is called is not through a 'spawnObject' call. The spawnObject function could for example create the C++ object instance and store it so that GetCPPInstance can access it.


shared abstract class ScriptObject: IController
{
     ScriptObject() 
     {
         @self = GetCPPInstance();
     }

     void render()
     {
         self.render();
     }

     void move()
     {
         self.move();
     }
}

The ScriptObject should preferrably be compiled once in an application provided module that is given a specific access mask to be allowed to call the GetCPPInstance function.

The other modules provided by the script writers need only have the following line to use the ScriptObject:


external shared class ScriptObject;

 

 

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

Thanks Andreas, sounds like it would do what I need indeed!

Only problem I have with this approach is how to pass the instance of C++ object between a call to spawnObject and call to GetCPPInstance(). I was thinking about something along these lines - spawnObject creates C++ instance and also instantiates script object, so these could be connected through a pointer, and C++ object could be parked there waiting for the call from script objects ctor, but I'm not sure the script pointer is available before the constructor finishes? Not sure this is not overcomplicating the matter. 

Ideal solution would be disallowing certain script classes to be instantiated by usual means inside script, and only allowing this through factory function on C++ side, but I assume this is not possible right now. 

I will try to get this working though and see if I manage to figure out how to glue this together. 


Where are we and when are we and who are we?
How many people in how many places at how many times?

Hmm... I'm trying this but have no idea how to not do too "entangled". Calling function in constructor of script object that throws exception sounds okayish (though I'd gladly not use exception for this and just early or plain prevent instantiation of such objects in scripts without special factory func) but passing spawned object pointer through some connection made through this call seems slightly complicated.

It looks like what we'd need is to make a class constructor private or in some other way impossible to instantiate from within script without being created through factory method on C++ side. Any ideas with pseudo code welcome :)

Ideally it would be compile-time - so if it sees instantiation of class that should not be instantiated directly, it just fails to compile, but this would require some language feature :(
 

UPDATE: Okay I think context could be used to pass that instance between spawning method and obtaining the pointer in ctor, right? User data for that single context should be the same... Trying that now.


Where are we and when are we and who are we?
How many people in how many places at how many times?

Just a thought. You might be able to declare the script class' constructor as private. That should prevent the scripts from being able to manually create instances of the class, but the spawn function will still be able to call the factory function from the application side to create the instance.

I haven't tried this myself though. Let me know if you encounter any issues.

 

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

I tried that but putting private keyword in front of a constructor causes syntax error - I think it's not expecting that keyword together with constructor, so when it sees private, next thing it expects is some return type not method name. 


class Foo 
{
    private Foo() { }
}

Expected identifier
Instead found '('
 Unexpected token '~'
 Unexpected token 'private'
 Unexpected token '}'

Unless I do something wrong, or my AS version is old (I haven't updated it in a while, will try to do it next as it's worth doing an update anyway).


Where are we and when are we and who are we?
How many people in how many places at how many times?

I'll take a look at it. I have a feeling it is a bug still present in the latest version.

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

I have now working solution where C++ instance is shared between spawnObject method and getCPPObject method through context user data. Seems to work fine, if I instantiate the object it throws exception that it should be instantiated through spawnObject. The last problem I have is with my concept of "one object, one file" - once I set hierarchy of objects deeper than 1, say:

object.as: abstract shared Object 

bar.as: Bar < Object

foo.as: Foo < Bar < Object

baz.as: Baz < Bar < Object

Now when I try to spawn baz.as which has:

#include "bar.as"

my routine to look for the class name to instantiate fails, as it was naive "first thing that has a base of class Object". It seems that for Baz base class will be Bar so I'd have to recurently check until I reach Object. Is there some way to retrieve the most bottom class in the hierarchy? Or any other method to figure out which type from given script should be instantiated (usually first one that's descendant of Object, but other types will be pulled by include).

PS. Found method DerivesFrom(type) which can be called in all encountered types, and it seems that GetObjectTypeCount() starts with bottom classes first, so it goes in an order that works fine for now. Other idea I'm considering is just enforcing naming so "some_item.as" has to define SomeItem class inside and that one will be instantiated when I do spawnObject("some_item.as") -> pros of this is that there will be no "magic guessing" :)


Where are we and when are we and who are we?
How many people in how many places at how many times?

The easiest solution I think would simply be to enforce the naming to match the name of the file, so the application can directly look up the correct type.

But you have a few different options that you may try as well:

1. Enumerate all types, and then for each of them find the lowest type in the class hierarchy (using DerivesFrom like you identified)

2. Use meta data to indicate the correct type to spawn for the file, then enumerate the types and look for the one with the matching meta data.

Do not rely on the order that the GetObjectTypeByIndex returns the types. It is just by chance that they are returned in the order you expect. I make no commitment that this won't change in a future update if there is a need for it.

 

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

This topic is closed to new replies.

Advertisement