1
\$\begingroup\$

I have a fairly standard architecture like this:

GameObjects have Transform, and can have Renderer, Rigidbody, Camera, LightSource, etc, and all of these classes have their corresponding methods.

  • GameObject has GetName, SetName, etc. and its own Transform.
  • Transform has GetPosition, SetPosition, etc.
  • Renderer has SetTexture, SetMaterial, etc. and references a Transform/GameObject.

If I want to implement Lua scripting using LuaBridge, and I would like to achieve an API similar to this...

function update()
    local gameObject = this()
    local renderer = gameObject:GetRenderer()
    renderer:SetTexture("Brick")
    local rigidbody = gameObject:GetRigidbody()
    rigidbody:SetMass(10)
    local transform = gameObject:GetTransform()
    transform:SetPosition(1, 10, -5)
end

...then which of these should I choose?

Solution A:

  • I bind every C++ class directly to Lua classes.

    • In this way I have to implement some extra scripting quality-of-life methods for the classes, so no big deal.

    • But it's not so easy to see whether a method is callable in Lua script, or is only available in native code. (for example I need to name method XYZ as LuaXYZ)

    • Methods which are only created so that scripting is made easier, are mixed with core functionality.

Solution B:

  • I create a wrapper class for every required class, like: Renderer->RendererHandler, Transform->Transform Handler, GameObject->GameObjectHandler, etc.

    • Lua and core functionality is separated, much easier to see what is the scripting API and what is not.

    • For every class I have to create another similar class which got a lot of methods, which are just calling the calling the same method in the wrapped class. Like GameObjectHandler->GetName() just calls GameObject->GetName()

Thanks in advance.

Summary:

  • In my opinion, Solution B is the best. That is the easiest to handle, the most transparent, and the most separated.

  • But I got lots of new classes and new dependencies, which messes up my clean architecture. Solution A makes expanding functionality much easier to handle (I dont have to create a lua interface class for new components).

  • But error handling is worse, because I don't have to care about if a scripter uses a NULL pointer (like a non existent component)

  • But if that happens due to my engine implementation I'll have to handle it seriously. (And how can I signal whether a NULL pointer in a method is "don't care type" or "serious problem" type?)

  • So currently I think that the best solution is this:

    class GameObject {
       ...
    
       GetName()
       SetName()
    
       Lua_getName()
       Lua_setName()
    
       ...
    
  • I don't have to create "Lua interface" classes, and handle that mess that comes with it.

  • I directly bind the Lua functions to the classes, but I create new methods in that class for scripting purposes.

  • For example I implement GetName() for "InEngine" functionality, and a Lua_getName() which is bound to Lua functions and used only in scripts.

  • The problem here is that, yeah you can't call InEngine methods from Lua because it's not bound. But what about calling Lua_getName() instead of GetName()?

    • There is no compile check for that, and if the developer is tired and makes this mistake, it will be hell to debug that.
\$\endgroup\$
  • \$\begingroup\$ In a programming language like C# or Java I would try to use annotations to mark methods which are supposed to be exposed to the scripting backend. But AFAIK C++ has nothing like that. \$\endgroup\$ – Philipp May 15 '17 at 10:57
  • \$\begingroup\$ I think this question is opinion-based. You should do whatever suits your own needs, there's no right or wrong approach in this. It completely depends on how you want the scripting to work. If the first approach you choose does not work, refactor it to the other solution you came up with. \$\endgroup\$ – Lasse May 17 '17 at 8:18
  • \$\begingroup\$ OP - are you setting up scripting for an artist, or for yourself / other coders to use? \$\endgroup\$ – Engineer May 17 '17 at 8:23
  • \$\begingroup\$ Mostly coders. I want to make a compile-once game engine, which dinamically loads the project, so it's easily moddable. 100 % of the game logic would be in scripts. \$\endgroup\$ – Tudvari May 17 '17 at 9:57
  • \$\begingroup\$ It's generally not a good idea to assume that a gameobject has all of those components if you're trying to create an entity-component system. Pay closer attention to your data, and combine items that tend to go together anyways. This is why I personally hate ECS as is on paper, it sounds great on paper, but is a pain to actually implement reasonably. \$\endgroup\$ – moonshineTheleocat May 19 '17 at 2:36
2
+50
\$\begingroup\$

C++ aliases seem to provide a solution to your problem.

Solution A

The main benefit here is low maintenance. The other is you can avoid method call indirection overheads by avoiding wrappers (I imagine that will have multi-JMP overheads, FWIW).

Problem 1 remains knowing what constitutes your Lua interface and what is internal, without having to wrap method calls. To get around that, I would suggest that you name the actual methods with the Lua- prefix, while giving these methods aliases that lack said prefix. Now your existing internal code can continue to use the old names, but you can see at a glance in your class which methods are Lua-accessible. Extra QoL methods sit neatly alongside these, with same Lua- prefix.

Problem 2 is nested method calls which should ideally be avoided, and while it is conceivable that your compiler of choice optimises these out, I wouldn't rely on it (especially optimising across distinct classes as in your Solution B). Fortunately aliases, as a compile-time mechanism, solve this problem.

I recommend this solution based on its simplicity.

Solution B

Provided the above doesn't work out for whatever reason, this is desirable as you suggest, in terms of maintaining a clean separation of interfaces (engine internals vs. scripting).

What you don't want is the hassle of maintaining this new layer. Frankly, you should not have to, for it ought to be easy enough to automate generation of that layer when changes occur. You'd need to integrate this with your build process, however - which is more work.

Philipp rightly suggests somehow annotating those methods which you wish some wrapper generator (SWIG) to process. Try putting some unmistakable string inside one-line comments preceding each such function, search for that string, and have the wrapper generator operate on that. I can't be more specific than this, as it is going to hinge on what generator you end up using and what its capabilities are in terms of reading annotations - e.g. SWIG, some other third party's, or your own home-brewed.

You might write your own text parser to run over source code and identify such methods, then pass names of those methods into your generator to generate necessary interfaces. Devil is in the details.

If your compiler does not optimise out the indirection of method calls through your wrapper class, you're going to end up with marginally worse performance, although negligible in 98% of cases.

Conclusion

While you are actively developing infrastructure, do not tie yourself into solutions that are going to increase the cost of change unless you have an exceedingly good reason to do so. You need to be able to remain agile through much of your production, even if codebase organisation is not ideal. In this case that means avoiding Solution B - even if you get it working nicely, at the very least you'll frequently have to run your interface generator. Provided that doesn't add much time to each build, or it's very infrequent, it may be okay for you... the problem there is, you have to implement it before you'll know. Catch-22 situation. My gut feel is increased complexity is best avoided at this (early?) stage. I also think the entire point of something like LuaBridge, which you are using, is to maintain agility.

\$\endgroup\$
  • \$\begingroup\$ I have a few questions: Isn't having a method with 2 name is bad design? It can be confusing, and why would anyone use Lua-XY or XY if they are the same? So what is the reason for having 2 names? Just because so I don't have to change the previously written code? And the main source of the problem is that for example: If GetData(int ID) is called by the engine it is 100% sure that ID is a valid parameter, because for example it found in array just before calling the function. But if it's called from Lua, there should be a check whether ID is valid. \$\endgroup\$ – Tudvari May 20 '17 at 22:56
  • \$\begingroup\$ So crashing without errors in Lua is unaccaptable, so I have to write a check in the functions. Let's say that this method is called very often inside the engine, but rarely from Lua. (Because for example it's a low level function, but this doesn't really matter). This way, the checking in 90 % of the calls is needless. \$\endgroup\$ – Tudvari May 20 '17 at 22:58
  • 1
    \$\begingroup\$ (a) Why would aliasing be supported in modern languages, and be a much-demanded feature, if bad design? Having two names addresses your concern that "it's not so easy to see whether a method is callable in Lua script, or is only available in native code." Sure, it does avoid having to change names elsewhere, but that's just a nice side-effect. (b) Here you have no choice but to check ID inside the Lua- function, which then runs the internal function if the check returns valid. Else as you say, it's inefficient the rest of the time. In such cases, wrap, in other cases, alias. But see below 1st. \$\endgroup\$ – Engineer May 21 '17 at 7:26
  • 1
    \$\begingroup\$ (c) For runtime assigned (integer) IDs, you should be grabbing a list of valid IDs from C++ and operating only on (subsets of) those in Lua. Lua scripts should generally not be operating on some arbitrary value - just write an internal method GetValidEntityIDs() for this, which Lua calls before any Lua_SomeFunc(ID) call which it calls across the entire list (just like in the engine internals for rendering etc.). Only human-entered IDs (via runtime CLI/shell) can be wrong, so we'd need special pre-checks and to wrap internal functions under the conditional. I'd use Shell- prefix here. \$\endgroup\$ – Engineer May 21 '17 at 7:43
  • \$\begingroup\$ I had a month break on this project and now I started working on it again. My first issue is that for example a Getter in my class returns const std::string instead of std::string. And LuaBridge can't convert from constant. Any fix on this? Thanks in advance! :) Update: using "const std::string &" as a parameter doesn't cause any problems. So only constant returns are problematic. \$\endgroup\$ – Tudvari Jul 6 '17 at 16:40

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.