#include "App.hpp" #include "AppConfig.hpp" #include "CommonVertexIndex.hpp" #include "ImGuiGuizmo.hpp" #include "Input.hpp" #include "Ires.hpp" #include "Level.hpp" #include "Log.hpp" #include "Material.hpp" #include "Shader.hpp" #define GLFW_INCLUDE_NONE #include #include #include #include #include #include #include #include #include #include #include #include #include namespace fs = std::filesystem; using namespace std::literals; struct GlfwUserData { App* app = nullptr; }; void GlfwErrorCallback(int error, const char* description) { fprintf(stderr, "[GLFW] Error %d: %s\n", error, description); } void GlfwKeyboardCallback(GLFWkeyboard* keyboard, int event) { if (InputState::instance == nullptr) { // Called before initialization, skipping because we'll do a collect pass anyways when initializing return; } switch (event) { case GLFW_CONNECTED: { InputState::instance->ConnectKeyboard(keyboard); } break; case GLFW_DISCONNECTED: { InputState::instance->DisconnectKeyboard(keyboard); } break; } } void OpenGLDebugCallback(GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length, const GLchar* message, const void* userParam) { fprintf(stderr, "GL CALLBACK: %s type = 0x%x, severity = 0x%x, message = %s\n", (type == GL_DEBUG_TYPE_ERROR ? "** GL ERROR **" : ""), type, severity, message); } void GlfwFramebufferResizeCallback(GLFWwindow* window, int width, int height) { AppConfig::mainWindowWidth = width; AppConfig::mainWindowHeight = height; AppConfig::mainWindowAspectRatio = (float)width / height; } void GlfwMouseCallback(GLFWwindow* window, int button, int action, int mods) { if (ImGui::GetIO().WantCaptureMouse) { return; } auto userData = static_cast(glfwGetWindowUserPointer(window)); auto app = userData->app; app->HandleMouse(button, action); } void GlfwMouseMotionCallback(GLFWwindow* window, double xOff, double yOff) { if (ImGui::GetIO().WantCaptureMouse) { return; } auto userData = static_cast(glfwGetWindowUserPointer(window)); auto app = userData->app; app->HandleMouseMotion(xOff, yOff); } void GlfwKeyCallback(GLFWwindow* window, int key, int scancode, int action, int mods) { if (ImGui::GetIO().WantCaptureKeyboard) { return; } GLFWkeyboard* keyboard = glfwGetLastActiveKeyboard(); if (keyboard) { auto userData = static_cast(glfwGetWindowUserPointer(window)); auto app = userData->app; app->HandleKey(keyboard, key, action); } } // For platform data path selection below // https://stackoverflow.com/questions/54499256/how-to-find-the-saved-games-folder-programmatically-in-c-c #if defined(_WIN32) # if defined(__MINGW32__) # include # else # include # endif # include # pragma comment(lib, "shell32.lib") # pragma comment(lib, "ole32.lib") #elif defined(__linux__) fs::path GetEnvVar(const char* name, const char* backup) { if (const char* path = std::getenv(name)) { fs::path dataDir(path); fs::create_directories(dataDir); return dataDir; } else { fs::path dataDir(backup); fs::create_directories(dataDir); return dataDir; } } #endif int main(int argc, char* argv[]) { using namespace Tags; #if BRUSSEL_DEV_ENV Log::gDefaultBuffer.messages.resize(1024); Log::gDefaultBufferId = Log::RegisterBuffer(Log::gDefaultBuffer); #endif constexpr auto kOpenGLDebug = "opengl-debug"; constexpr auto kImGuiBackend = "imgui-backend"; constexpr auto kGameDataDir = "game-data-directory"; constexpr auto kGameAssetDir = "game-asset-directory"; cxxopts::Options options(std::string(AppConfig::kAppName), ""); // clang-format off options.add_options() (kOpenGLDebug, "Enable OpenGL debugging messages.") (kImGuiBackend, "ImGui backend. Options: opengl2, opengl3. Leave empty to default.", cxxopts::value()) (kGameAssetDir, "Directory in which assets are looked up from. Can be relative paths to the executable.", cxxopts::value()->default_value(".")) (kGameDataDir, "Directory in which game data (such as saves and options) are saved to. Leave empty to use the default directory on each platform.", cxxopts::value()) ; // clang-format on auto args = options.parse(argc, argv); bool imguiUseOpenGL3; if (args.count(kImGuiBackend) > 0) { auto imguiBackend = args[kImGuiBackend].as(); if (imguiBackend == "opengl2") { imguiUseOpenGL3 = false; } else if (imguiBackend == "opengl3") { imguiUseOpenGL3 = true; } else { // TODO support more backends? imguiUseOpenGL3 = true; } } else { imguiUseOpenGL3 = true; } if (args.count(kGameAssetDir) > 0) { auto assetDir = args[kGameAssetDir].as(); fs::path assetDirPath(assetDir); if (!fs::exists(assetDirPath)) { fprintf(stderr, "Invalid asset directory.\n"); return -4; } AppConfig::assetDir = std::move(assetDir); AppConfig::assetDirPath = std::move(assetDirPath); } else { AppConfig::assetDir = "."; AppConfig::assetDirPath = fs::path("."); } if (args.count(kGameDataDir) > 0) { auto dataDir = args[kGameDataDir].as(); fs::path dataDirPath(dataDir); fs::create_directories(dataDir); AppConfig::dataDir = std::move(dataDir); AppConfig::dataDirPath = std::move(dataDirPath); } else { #if BRUSSEL_DEV_ENV AppConfig::dataDir = "."; AppConfig::dataDirPath = fs::path("."); #else // In a regular build, use default platform data paths # if defined(_WIN32) fs::path dataDirPath; PWSTR path = nullptr; HRESULT hr = SHGetKnownFolderPath(FOLDERID_RoamingAppData, KF_FLAG_CREATE, nullptr, &path); if (SUCCEEDED(hr)) { dataDirPath = fs::path(path) / AppConfig::kAppName; CoTaskMemFree(path); fs::create_directories(dataDirPath); } else { std::string msg; msg += "Failed to find/create the default user data directory at %APPDATA%. Error code: "; msg += hr; throw std::runtime_error(msg); } # elif defined(__APPLE__) // MacOS programming guide recommends apps to hardcode the path - user customization of "where data are stored" is done in Finder auto dataDirPath = fs::path("~/Library/Application Support/") / AppConfig::kAppName; fs::create_directories(dataDirPath); # elif defined(__linux__) auto dataDirPath = GetEnvVar("XDG_DATA_HOME", "~/.local/share") / AppConfig::kAppName; fs::create_directories(dataDirPath); # endif AppConfig::dataDir = dataDirPath.string(); AppConfig::dataDirPath = dataDirPath; #endif } if (!glfwInit()) { return -1; } glfwSetErrorCallback(&GlfwErrorCallback); glfwSetKeyboardCallback(&GlfwKeyboardCallback); glfwWindowHint(GLFW_CLIENT_API, GLFW_OPENGL_API); glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); glfwWindowHint(GLFW_RESIZABLE, GLFW_TRUE); #if defined(__APPLE__) const char* imguiGlslVersion = "#version 150"; glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); // Required on Mac #else const char* imguiGlslVersion = "#version 130"; glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); #endif GlfwUserData glfwUserData; GLFWwindow* window = glfwCreateWindow(1280, 720, AppConfig::kAppNameC, nullptr, nullptr); if (window == nullptr) { return -2; } glfwSetWindowUserPointer(window, &glfwUserData); // Window callbacks are retained by ImGui GLFW backend glfwSetFramebufferSizeCallback(window, &GlfwFramebufferResizeCallback); glfwSetKeyCallback(window, &GlfwKeyCallback); glfwSetMouseButtonCallback(window, &GlfwMouseCallback); glfwSetCursorPosCallback(window, &GlfwMouseMotionCallback); { int width, height; glfwGetFramebufferSize(window, &width, &height); GlfwFramebufferResizeCallback(window, width, height); } glfwMakeContextCurrent(window); glfwSwapInterval(1); if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) { return -3; } #if defined(BRUSSEL_DEV_ENV) auto glVersionString = glGetString(GL_VERSION); int glMajorVersion; glGetIntegerv(GL_MAJOR_VERSION, &glMajorVersion); int glMinorVersion; glGetIntegerv(GL_MINOR_VERSION, &glMinorVersion); printf("OpenGL version (via glGetString(GL_VERSION)): %s\n", glVersionString); printf("OpenGL version (via glGetIntegerv() with GL_MAJOR_VERSION and GL_MINOR_VERSION): %d.%d\n", glMajorVersion, glMinorVersion); #endif bool useOpenGLDebug = args[kOpenGLDebug].as(); if (useOpenGLDebug) { printf("Using OpenGL debugging\n --%s", kOpenGLDebug); // TODO check extension KHR_debug availability // TODO conan glad is not including any extensions // NOTE: KHR_debug is a core extension, which means it may be available in lower version even though the feature is added in 4.3 glEnable(GL_DEBUG_OUTPUT); glDebugMessageCallback(&OpenGLDebugCallback, 0); } IMGUI_CHECKVERSION(); auto ctx = ImGui::CreateContext(); auto& io = ImGui::GetIO(); ImGuizmo::SetImGuiContext(ctx); ImGui_ImplGlfw_InitForOpenGL(window, true); if (imguiUseOpenGL3) { ImGui_ImplOpenGL3_Init(imguiGlslVersion); } else { ImGui_ImplOpenGL2_Init(); } InputState::instance = new InputState(); { int count; GLFWkeyboard** list = glfwGetKeyboards(&count); for (int i = 0; i < count; ++i) { GLFWkeyboard* keyboard = list[i]; InputState::instance->ConnectKeyboard(keyboard); } } IresManager::instance = new IresManager(); IresManager::instance->DiscoverFilesDesignatedLocation(); LevelManager::instance = new LevelManager(); LevelManager::instance->DiscoverFilesDesignatedLocation(); gVformatStandard.Attach(new VertexFormat()); gVformatStandard->AddElement(VertexElementFormat{ .bindingIndex = 0, .type = VET_Float3, .semantic = VES_Position, }); gVformatStandard->AddElement(VertexElementFormat{ .bindingIndex = 0, .type = VET_Float2, .semantic = VES_TexCoords1, }); gVformatStandard->AddElement(VertexElementFormat{ .bindingIndex = 0, .type = VET_Ubyte4Norm, .semantic = VES_Color1, }); gVformatStandardSplit.Attach(new VertexFormat()); gVformatStandardSplit->AddElement(VertexElementFormat{ .bindingIndex = 0, .type = VET_Float3, .semantic = VES_Position, }); gVformatStandardSplit->AddElement(VertexElementFormat{ .bindingIndex = 1, .type = VET_Float2, .semantic = VES_TexCoords1, }); gVformatStandardSplit->AddElement(VertexElementFormat{ .bindingIndex = 1, .type = VET_Ubyte4Norm, .semantic = VES_Color1, }); gVformatLines.Attach(new VertexFormat()); gVformatLines->AddElement(VertexElementFormat{ .bindingIndex = 0, .type = VET_Float3, .semantic = VES_Position, }); gVformatLines->AddElement(VertexElementFormat{ .bindingIndex = 0, .type = VET_Ubyte4Norm, .semantic = VES_Color1, }); // Matches gVformatStandard gDefaultShader.Attach(new Shader()); gDefaultShader->InitFromSources(Shader::ShaderSources{ .vertex = R"""( #version 330 core layout(location = 0) in vec3 pos; layout(location = 1) in vec4 color; out vec4 v2fColor; uniform mat4 transform; void main() { gl_Position = transform * vec4(pos, 1.0); v2fColor = color; } )"""sv, .fragment = R"""( #version 330 core in vec4 v2fColor; out vec4 fragColor; void main() { fragColor = v2fColor; } )"""sv, }); { // in vec3 pos; ShaderMathVariable var; var.scalarType = GL_FLOAT; var.width = 1; var.height = 3; var.arrayLength = 1; var.semantic = VES_Position; var.location = 0; gDefaultShader->GetInfo().inputs.push_back(std::move(var)); gDefaultShader->GetInfo().things.try_emplace( "pos"s, ShaderThingId{ .kind = ShaderThingId::KD_Input, .index = (int)gDefaultShader->GetInfo().inputs.size() - 1, }); } { // in vec4 color; ShaderMathVariable var; var.scalarType = GL_FLOAT; var.width = 1; var.height = 4; var.arrayLength = 1; var.semantic = VES_Color1; var.location = 1; gDefaultShader->GetInfo().inputs.push_back(std::move(var)); gDefaultShader->GetInfo().things.try_emplace( "color"s, ShaderThingId{ .kind = ShaderThingId::KD_Input, .index = (int)gDefaultShader->GetInfo().inputs.size() - 1, }); } { // out vec4 fragColor; ShaderMathVariable var; var.scalarType = GL_FLOAT; var.width = 1; var.height = 4; var.arrayLength = 1; gDefaultShader->GetInfo().outputs.push_back(std::move(var)); gDefaultShader->GetInfo().things.try_emplace( "fragColor"s, ShaderThingId{ .kind = ShaderThingId::KD_Output, .index = (int)gDefaultShader->GetInfo().outputs.size() - 1, }); } // NOTE: autofill uniforms not recorded here gDefaultMaterial.Attach(new Material()); gDefaultMaterial->SetShader(gDefaultShader.Get()); { // Main loop App app; glfwUserData.app = &app; // NOTE: don't enable backface culling, because the game mainly runs in 2D and sometimes we'd like to flip sprites around // it also helps with debugging layers in 3D view glEnable(GL_DEPTH_TEST); // 60 updates per second constexpr double kMsPerUpdate = 1000.0 / 60; constexpr double kSecondsPerUpdate = kMsPerUpdate / 1000; double prevTime = glfwGetTime(); double accumulatedTime = 0.0; while (!glfwWindowShouldClose(window)) { { ZoneScopedN("GameInput"); glfwPollEvents(); } double currTime = glfwGetTime(); double deltaTime = prevTime - currTime; // In seconds accumulatedTime += currTime - prevTime; // Update // Play "catch up" to ensure a deterministic number of Update()'s per second while (accumulatedTime >= kSecondsPerUpdate) { double beg = glfwGetTime(); { ZoneScopedN("GameUpdate"); app.Update(); } double end = glfwGetTime(); // Update is taking longer than it should be, start skipping updates auto diff = end - beg; if (diff >= kSecondsPerUpdate) { auto skippedUpdates = (int)(accumulatedTime / kSecondsPerUpdate); accumulatedTime = 0.0; fprintf(stderr, "Elapsed time %f, skipped %d updates.", diff, skippedUpdates); } else { accumulatedTime -= kSecondsPerUpdate; } } int fbWidth = AppConfig::mainWindowWidth; int fbHeight = AppConfig::mainWindowHeight; glfwGetFramebufferSize(window, &fbWidth, &fbHeight); glViewport(0, 0, fbWidth, fbHeight); auto clearColor = ImVec4(0.45f, 0.55f, 0.60f, 1.00f); glClearColor(clearColor.x * clearColor.w, clearColor.y * clearColor.w, clearColor.z * clearColor.w, clearColor.w); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); { // Regular draw ZoneScopedN("Render"); app.Draw(currTime, deltaTime); } { // ImGui draw ZoneScopedN("ImGui"); if (imguiUseOpenGL3) { ImGui_ImplOpenGL3_NewFrame(); } else { ImGui_ImplOpenGL2_NewFrame(); } ImGui_ImplGlfw_NewFrame(); ImGui::NewFrame(); app.Show(); ImGui::Render(); if (imguiUseOpenGL3) { ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); } else { ImGui_ImplOpenGL2_RenderDrawData(ImGui::GetDrawData()); } } glfwSwapBuffers(window); FrameMark; prevTime = currTime; } } if (imguiUseOpenGL3) { ImGui_ImplOpenGL3_Shutdown(); } else { ImGui_ImplOpenGL2_Shutdown(); } ImGui_ImplGlfw_Shutdown(); ImGui::DestroyContext(); glfwDestroyWindow(window); glfwTerminate(); return 0; }