c++oopdllgame-enginegame-development

Allocating an Unknown Inherited Class from another Dynamic Library


I'm working on a small game engine project and I want to make an entity allocationg system.

The idea is this: Engine DLL is "unchangeable", provided by me. Developers will be able to compile Game DLL themselves with custom entity types.

Every Entity is derived from CBase class of the Engine. Entities have class names, which will be used when reading the map. Entities will be "spawned" using those class names. It's basically an alias for that entity.

But there's a problem:

Allocation of "spawned" entites should be done in the engine. Engine should be able to call fundamental functions, and will not be able to call additional functions.

But Engine doesn't import these classes from the Game DLL, so I cant just use new EntityClass

How can I allocate unknown classes (derived from a known class) from the other DLL? Is there a way to "pass a type" through functions?


My workaround before was like this pseudocode:

Engine:

class DLLEXPORT CBase
{
  std::string className;

  virtual void Init();
  virtual void OtherMethod();
}

CBase* CreateNewEntity(const char* classname)
{
  CBase* newentity = static_cast<CBase*>(SERVER_DLL_GETNEWOBJECT(classname));
  newentity->Init();
  entitylist.addentity(newentity);
}

Game DLL:

class DLLEXPORT CBaseEntity : public DLLIMPORT CBase
{
  //Called by engine
  virtual void Init() override;
  virtual void OtherFunction() override;

  //Inaccesible by engine
  virtual void AnotherFunction() override;
}

CBaseEntity* SERVER_DLL_GETNEWOBJECT(const char* classname)
{
  if (strcmp(className, "Entity_Weapon_Test") == 0)
  {
      return static_cast<CBaseEntity*>(new CTestWeapon);
  }
  else
      return nullptr;
}

SERVER_DLL_GETNEWOBJECT is exported from Game DLL, and imported into the Server DLL. It was going to be a predefined function.

But in this workaround, the allocation is done by the Game DLL, not by the engine. So I can't use it.


Solution

  • I would expect something along these lines :

    #include <memory>
    #include <string>
    #include <functional>
    #include <map>
    #include <stdexcept>
    #include <iostream>
    
    // Declare an "interface" (abstract baseclass)
    // this is the only knowledge your engine
    // and client code will share
    //
    // But make sure client code compiles with an ABI
    // compatible compiler!
    //
    class EntityItf
    {
    public:
        virtual ~EntityItf() = default;
        virtual void DoSomething() = 0;
    };
    
    using creation_function_t = std::function<std::unique_ptr<EntityItf>()>;
    
    class /*dllexport */ EntityFactory
    {
    public:
        // Meyers singleton
        inline static EntityFactory& Instance()
        {
            static EntityFactory factory;
            return factory;
        }
    
        void RegisterCreationMethod(const std::string& entity_name, creation_function_t creation_function)
        {
            if ( m_factory_functions.find(entity_name) != m_factory_functions.end())
            {
                throw std::runtime_error{"An entity with that name has already been registered"};
            }
    
            m_factory_functions[entity_name] = creation_function;
        }
    
        std::unique_ptr<EntityItf> CreateEntity(const std::string& entity_name)
        {
            if ( m_factory_functions.find(entity_name) == m_factory_functions.end())
            {
                throw std::runtime_error{"Unknown entity name"};
            }
    
            return m_factory_functions[entity_name]();
        }
    
    private:
        EntityFactory() = default;
        ~EntityFactory() = default;
    
        std::map<std::string,creation_function_t> m_factory_functions;
    };
    
    // Developer code
    
    class MyEntity :
        public EntityItf
    {
    public:
        void DoSomething() override
        {
            std::cout << "Hello World!\n";
        }
    };
    
    int main()
    {
        auto& factory = EntityFactory::Instance();
        factory.RegisterCreationMethod("MyEntity", []{ return std::make_unique<MyEntity>(); });
    
        auto my_entity = factory.CreateEntity("MyEntity");
        my_entity->DoSomething();
    
        return 0;
    }