Я спрашиваю себя, как можно добиться чего-то в игровом движке, где пользователь просто определяет один класс и может его компилировать. Может быть, позвольте мне уточнить код, это будет то, что делает пользователь игрового движка:
#include <engine.h>
class game : engine::application
{
void main_loop() override;
void initialize() override;
void destruct() override;
};
как мне добиться того, чтобы игровой движок мог быть скомпилирован в своего рода библиотеку или пакет, при этом движок все еще мог получить доступ к экземпляру или чему-то вроде синглтона этого игрового класса. Итак, сначала вы скомпилируете движок, а затем сможете использовать его в своей игре.
Я пробовал метод с использованием ключевого слова extern, когда пользователь просто определяет такую функцию:
engine::application* get_game() {return new game();}
но его довольно грязно использовать, например, я не знаю почему, но вы должны написать код, который использует эту функцию в заголовочном файле, потому что, когда вы компилируете код в исходном файле, он не будет знать об этой функции и скажет undefined и что-то . Я также пробовал некоторые исследования, но действительно ничего не нашел по этой теме.
Конструктор по умолчанию для engine::application может хранить свой указатель this в подходящем месте (а также выдавать ошибку, если вы попытаетесь создать вторую игру). Поскольку все методы game являются виртуальными, сохраненный указатель может использоваться для доступа к методам game.
извините, но я действительно не понимаю, как движок сможет получить доступ к чему-либо из игры таким образом
@ignatz - Конструктор game вызовет конструктор для engine::application, который может отслеживать, вызывался ли он ранее, и рано выдавать ошибку. Нет реальной необходимости использовать сложный синглтон с указателями или чем-то еще. Просто создайте объект game и используйте его.
@ignatz Поскольку именно так работают виртуальные функции, вам не нужен указатель на game для доступа к виртуальным функциям game, просто указатель на базовый класс game, например engine::application.





Вы можете черпать вдохновение из нескольких фреймворков модульного тестирования. Обычно есть какой-то макрос для определения тестовых случаев. Обычно это приводит к созданию глобального объекта, который обращается к синглтону, хранящему информацию о зарегистрированных модульных тестах. (Это делают и ускоренный тест, и GoogleTest.)
В вашем случае вам просто нужно изменить это для одного экземпляра, и все, что вам нужно сделать в реализации игры, это что-то вроде этого (application уменьшено до одной функции-члена для этой демонстрации).
игра.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);
Здесь я использую проект cmake и предполагаю использование MSVC для Windows (не проверено для других компиляторов/систем).
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)
двигатель.л.с.
#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
Вы можете предоставить функцию
main()в своей библиотеке, которая вызывает такой интерфейс, предоставленный пользователем как определение класса. Так поступают многие библиотеки для игровых движков или программы запуска тестового кода.