Search code examples
c++game-engineprogram-entry-pointextern

entrypoint in game engine


I am asking myself, how you would achieve something in a game engine, where a user just defines one class and can compile. Maybe let me elaborate with some code, this would just be what the user of the game engine does:

#include <engine.h>

class game : engine::application
{
    void main_loop() override;
    void initialize() override;
    void destruct() override;
};

how would i achieve it, that the game engine could be compiled into a library or package of sorts, with the engine still being able to access an instance or something like the singleton of this game class. So first youd compile the engine, and then be able to use it with your game.

I've tried a method using the extern keyword where the user just defines a function like this:

engine::application* get_game() {return new game();}

but its quite messy to use, like i dont know why but you have to write code that uses this function in a header file, cause when you compile the code in a source file, it would not know of this function and say undefined and something. I also tried some research, but really found nothing on this topic.


Solution

  • You could take inspiration from several unit test frameworks out there. Usually there's some kind of macro for defining test cases. Usually those result in the creation of a global object that accesses a singleton storing the info about registered unit tests. (Boost test and GoogleTest both do this.)

    In your case you'd just need to modify this for a single instance and all you need to do in the game implementation is something like this (application reduced to a single member function for this demo).

    game.cpp

    #include <iostream>
    
    #include "engine/engine.hpp"
    
    class my_game : public engine::application
    {
    public:
        void DoSomething() override
        {
            std::cout << "Hello World!\n";
        }
    };
    
    ENGINE_GAME(my_game);
    

    Engine Implementation

    I'm using a cmake project here and assume usage of MSVC for Windows (untested for other compilers/systems).

    CMakeLists.txt

    cmake_minimum_required(VERSION 3.16)
    
    project(EngineDemo)
    
    # use the same dir for dlls and exes to avoid issues with finding the dll
    set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR})
    
    include(GenerateExportHeader)
    
    add_library(engine SHARED engine.cpp include/engine/engine.hpp)
    target_include_directories(engine PUBLIC include ${CMAKE_CURRENT_BINARY_DIR}/include)
    generate_export_header(engine BASE_NAME include/engine/engine EXPORT_MACRO_NAME ENGINE_EXPORT)
    target_sources(engine PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/include/engine/engine_export.h)
    target_compile_features(engine PUBLIC cxx_std_17)
    
    add_executable(game game.cpp)
    target_link_libraries(game PRIVATE engine)
    

    engine.hpp

    #ifndef ENGINE_ENGINE_HPP
    #define ENGINE_ENGINE_HPP
    
    #include <cassert>
    
    #include "engine/engine_export.h"
    
    int ENGINE_EXPORT main();
    
    namespace engine
    {
    template<class T>
    struct application_registrar;
    
    class application;
    
    // std::unique_ptr has no dll interface, so we implement our own
    class ENGINE_EXPORT application_ptr
    {
        application* m_ptr;
    public:
        application_ptr(application* app = nullptr) noexcept
            : m_ptr(app)
        {
        }
    
        ~application_ptr();
    
        application_ptr(application_ptr&& other) noexcept
            : m_ptr(other.m_ptr)
        {
            other.m_ptr = nullptr;
        }
    
        application_ptr& operator=(application_ptr&& other) noexcept;
    
        explicit operator bool() const noexcept
        {
            return m_ptr != nullptr;
        }
    
        application* operator->() const noexcept
        {
            assert(m_ptr != nullptr);
            return m_ptr;
        }
    
        application& operator*() const noexcept
        {
            assert(m_ptr != nullptr);
            return *m_ptr;
        }
    
    };
    
    class ENGINE_EXPORT application
    {
        template<class T>
        friend struct application_registrar;
    
        friend int ::main();
    
        static application_ptr s_applicationInstance;
    public:
        virtual ~application() = 0;
    
        virtual void DoSomething() = 0;
    
        static application& instance() noexcept
        {
            [[maybe_unused]] bool const applicationAlreadyRegistered = static_cast<bool>(application::s_applicationInstance);
            assert(applicationAlreadyRegistered);
            return *s_applicationInstance;
        }
    };
    
    template<class T>
    struct application_registrar
    {
        application_registrar()
        {
            [[maybe_unused]] bool const applicationAlreadyRegistered = static_cast<bool>(application::s_applicationInstance);
            assert(!applicationAlreadyRegistered);
            try
            {
                application::s_applicationInstance = application_ptr(new T());
            }
            catch (...)
            {
                assert(!"an exception was thrown initializing the application");
                throw;
            }
        }
    };
    
    inline application_ptr::~application_ptr()
    {
        delete m_ptr;
    }
    
    inline application_ptr& application_ptr::operator=(application_ptr&& other) noexcept
    {
        delete m_ptr;
        m_ptr = other.m_ptr;
        other.m_ptr = nullptr;
        return *this;
    }
    
    } // namespace engine
    
    /**
     * macro for registering the game
     */
    #define ENGINE_GAME(type)                                                           \
    namespace EngineGameRegistrationImpl                                                \
    {                                                                                   \
    static ::engine::application_registrar<type> g_gameRegistrator##type##__LINE__ ;    \
    }
    
    #endif // ENGINE_ENGINE_HPP