diff options
Diffstat (limited to 'ProjectBrussel/Game/main.cpp')
-rw-r--r-- | ProjectBrussel/Game/main.cpp | 461 |
1 files changed, 461 insertions, 0 deletions
diff --git a/ProjectBrussel/Game/main.cpp b/ProjectBrussel/Game/main.cpp new file mode 100644 index 0000000..c49fc0b --- /dev/null +++ b/ProjectBrussel/Game/main.cpp @@ -0,0 +1,461 @@ +#include "App.hpp" + +#include "AppConfig.hpp" +#include "CommonVertexIndex.hpp" +#include "EditorGuizmo.hpp" +#include "Ires.hpp" +#include "Level.hpp" +#include "Material.hpp" +#include "Shader.hpp" + +#define GLFW_INCLUDE_NONE +#include <GLFW/glfw3.h> + +#include <backends/imgui_impl_glfw.h> +#include <backends/imgui_impl_opengl2.h> +#include <backends/imgui_impl_opengl3.h> +#include <glad/glad.h> +#include <imgui.h> +#include <imgui_internal.h> +#include <cstdlib> +#include <cxxopts.hpp> +#include <filesystem> +#include <string> + +#include <tracy/Tracy.hpp> +#include <tracy/TracyClient.cpp> + +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 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<GlfwUserData*>(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<GlfwUserData*>(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<GlfwUserData*>(glfwGetWindowUserPointer(window)); + auto app = userData->app; + app->HandleKey(keyboard, key, action); + } +} + +// https://stackoverflow.com/questions/54499256/how-to-find-the-saved-games-folder-programmatically-in-c-c +#if defined(_WIN32) +# if defined(__MINGW32__) +# include <ShlObj.h> +# else +# include <ShlObj_core.h> +# endif +# include <objbase.h> +# 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; + + 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() + (kImGuiBackend, "ImGui backend. Options: opengl2, opengl3. Leave empty to default.", cxxopts::value<std::string>()) + (kGameAssetDir, "Directory in which assets are looked up from. Can be relative paths to the executable.", cxxopts::value<std::string>()->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<std::string>()) + ; + // clang-format on + auto args = options.parse(argc, argv); + + bool imguiUseOpenGL3; + if (args.count(kImGuiBackend) > 0) { + auto imguiBackend = args[kImGuiBackend].as<std::string>(); + 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<std::string>(); + + 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 { +#if defined(_WIN32) + fs::path dataDir; + + PWSTR path = nullptr; + HRESULT hr = SHGetKnownFolderPath(FOLDERID_RoamingAppData, KF_FLAG_CREATE, nullptr, &path); + if (SUCCEEDED(hr)) { + dataDir = fs::path(path) / AppConfig::kAppName; + CoTaskMemFree(path); + + fs::create_directories(dataDir); + } 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 dataDir = fs::path("~/Library/Application Support/") / AppConfig::kAppName; + fs::create_directories(dataDir); +#elif defined(__linux__) + auto dataDir = GetEnvVar("XDG_DATA_HOME", "~/.local/share") / AppConfig::kAppName; + fs::create_directories(dataDir); +#endif + } + + if (args.count(kGameDataDir) > 0) { + auto dataDir = args[kGameDataDir].as<std::string>(); + + fs::path dataDirPath(dataDir); + fs::create_directories(dataDir); + + AppConfig::dataDir = std::move(dataDir); + AppConfig::dataDirPath = std::move(dataDirPath); + } else { + // TODO platform default path + AppConfig::dataDir = "."; + AppConfig::dataDirPath = fs::path("."); + } + + if (!glfwInit()) { + return -1; + } + + glfwSetErrorCallback(&GlfwErrorCallback); + + 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, "ImGui Command Palette Example", 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); + + // TODO setup opengl debug context + if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) { + return -3; + } + + 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(); + } + + 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, + }); + + // 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; +} |