Patterns for scripting in C++ when prototyping and testing components?

Started by
0 comments, last by frob 8 years ago

I’m using the C++ interpreter Cling embedded in my game (i.e. a REPL for C++11) to test some snippets and tweak values on the fly while the game is running. Since Cling also allows to load/unload cpp files at runtime I was thinking on creating a component that allows to hook callbacks loaded through the Cling interpreter like so:

Subclassing the component in the game's entity-component-system


/* The component */
class ClingComponent : Component
{
public:
    ClingComponent();
    virtual ~ClingComponent();

    virtual void update(float delta);

    FunctionPointer m_update;
};

void ClingComponent::update(float delta)
{
    if (m_update) {
        m_update(delta);
    }
}

Initializing the component


/* Load the callbacks and hook them up to the component */
ClingComponent *MyComponent = new ClingComponent();

snprintf(buff, sizeof(buff), "auto MyComponent = static_cast<ClingComponent*>((void*)%p);", MyComponent);

m_interpreter->process(buff);
m_interpreter->loadFile("MyCallbacks.cpp");

MyEntity->addComponent(MyComponent);

The C++ script loaded by Cling


/* MyCallbacks.cpp */
FunctionPointer updateCallback(float delta)
{
    // do something
}

MyComponent->m_update = updateCallback;
Advertisement

Since the only question is a vague one in your heading and not your discussion post, it seems like you are asking about adding some known methods to your interface, but I'm not quite certain that is what you are asking.

Yes, the pattern is common. It is used in dynamic libraries (DLLs), COM interfaces, and most plug-in architectures. It is the basic principle behind operating system device drivers and nearly every other system that allows for modules to be loaded and used.

Typically there will be several hooks that are called.

A few ideas:

OnLoad() -- called when the thing is loaded into memory. Probably called once per file rather than per instance. Registers with all the appropriate game systems that whatever is loaded is present and can be used.

OnUnload() -- called just before the thing is unloaded from memory. Same as OnLoad(), except removal.

RunSelfTest() -- Run automated self-tests, since that seems to be one of your questions.

QueryFunctions() -- Since C++ doesn't support reflection, this allows you to return the exposed function names and signatures in a way your architecture understands. It might be a series of function callbacks coupled with human-readable callback names, or a structure you fill out that replaces default function pointers with your own function pointers, or something else.

I don't think a per-component Update() function is a good idea because you will call it very frequently with minimal benefit. Better to have a registration system where only objects needing to be updated have an update function called.

I'd also mention the Dependency Inversion Principle which pairs with the Liskov Substitution Principle. This type of system works well when you provide an abstract base class and only operate on that base class. Dependency Inversion means your code only works with the abstract base classes, not with the concrete leaf classes. The Liskov Substitution Principle means that whatever sub-classes are created will behave properly and interchangeably with the abstract base class.

As an example most game programmers will understand, your code requests a Direct3D device of type ID3D11Device*. It is a generic base type that all the devices implement. It may have an underlying type of NVidiaGtx480Device*, or Ati6970Device*, or IntelHD530Device*, or some other concrete type that implements the ID3D11Device interface. You don't know or care what the implementation details are, you just know that it implements the ID3D11Device interface. Everything you do with the device is done with the ID3D11Device interface, never with the actual concrete type. Dependency Inversion means you can do everything you need to do with the base ID3D11Device pointer. The Liskov Substitution means whatever object you happen to actually have doesn't matter, it will do all the things you tell it to do.

Similarly with your component, you would not write ClingComponent* anywhere (except within the object itself, and perhaps in one line where you create and register the interface). ClingComponent is not the base type, you would only work with Component* objects. With dependency inversion, the code should never know or care about the implementation detail of the leaf type, only the base type. If you ever find yourself wanting to know details about the leaf type you either should change your thinking so you follow the substitution principle better, or you need to expand your interface so include the needed functionality.

This topic is closed to new replies.

Advertisement