I am currently developing a C++ OpenGL game engine to help myself learn computer graphics and game design later on. However, this project is becoming really big, and I am unsure if I am making the correct design choices with the code. Parts of the code are also becoming harder to add to, especially the parts that deal with UI.
Please be really harsh on it, as I really want to learn what I should be doing when making a program this big.
Link: https://github.com/Kirugaming/ProjectBahamut2/tree/map-geometry
Here is some code files that you could review:
Engine.cpp
// // Created by kirut on 10/16/2023. // #include "Engine.h" #include "backends/imgui_impl_sdl2.h" #include "backends/imgui_impl_opengl3.h" Engine::Engine(Project &chosenProject) : project(chosenProject), windowSize(displayMode.h-50, (displayMode.w) != 0) { SDL_GetCurrentDisplayMode(0, &displayMode); if (initRendering( (int) windowSize.height, (int) windowSize.width)) { std::cout << "ENGINE_ERROR::INIT_RENDERING" << std::endl; } baseShader = new Shader(); // init game game.camera = Camera(glm::vec3(0.0f, 0.0f, 2.0f)); game.level = new Level(); // init engine ui glViewport(windowSize.height / 6, windowSize.width / 6, windowSize.height / 1.5, windowSize.width / 1.5); glEnable(GL_DEPTH_TEST); glDepthFunc(GL_LESS); glEnable(GL_DEBUG_OUTPUT); } /* * Startup SDL rendering and OpenGL rendering */ int Engine::initRendering(int winHeight, int winWidth) { // Create Window window = SDL_CreateWindow("ProjectBahamut", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, winWidth, winHeight, SDL_WINDOW_SHOWN | SDL_WINDOW_RESIZABLE | SDL_WINDOW_OPENGL | SDL_WINDOW_MAXIMIZED); // opengl SDL_GL_LoadLibrary(nullptr); SDL_GL_SetAttribute( SDL_GL_CONTEXT_MAJOR_VERSION, 4 ); SDL_GL_SetAttribute( SDL_GL_CONTEXT_MINOR_VERSION, 6 ); SDL_GL_SetAttribute( SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE ); glContext = SDL_GL_CreateContext(window); // Init Glad if (!gladLoadGLLoader(reinterpret_cast<GLADloadproc>(SDL_GL_GetProcAddress))) { std::cout << "Glad failed to Initialize!" << std::endl; return -1; } return 0; } void Engine::engineLoop() { while(!game.quit){ game.computeDeltaTime(); // Events SDL_PumpEvents(); eventMonitor(); // query SDL events KeyboardInput(); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); glClearColor(.2f, .3f, .3f, 1.0f); baseShader->use(); for (Brush *brush: game.level->brushList) { drawMeshSubClass(brush); } drawGameObjects(game.level->gameObjects); baseShader->unUse(); ui->renderUI(&game); SDL_GL_SwapWindow(window); } } void Engine::eventMonitor() { while( SDL_PollEvent( &event ) ) { ImGui_ImplSDL2_ProcessEvent(&event); // Forward your event to backend switch (event.type) { case SDL_QUIT: game.quit = true; break; case SDL_WINDOWEVENT: switch (event.window.event) { case SDL_WINDOWEVENT_RESIZED: windowSize.height = event.window.data1; windowSize.width = event.window.data2; break; } break; } } } Engine::~Engine() { // cleanup SDL_GL_DeleteContext(glContext); SDL_DestroyWindow( window ); SDL_Quit(); } void Engine::KeyboardInput() { // possible to do this elsewhere but its here for now if (inputManager.getKeyDown("w")) { game.camera.movement(Camera::FORWARD, game.deltaTime); } if (inputManager.getKeyDown("s")) { game.camera.movement(Camera::BACKWARD, game.deltaTime); } if (inputManager.getKeyDown("a")) { game.camera.movement(Camera::LEFT, game.deltaTime); } if (inputManager.getKeyDown("d")) { game.camera.movement(Camera::RIGHT, game.deltaTime); } if (inputManager.getKeyDown("up")) { game.camera.setPitch(1); } if (inputManager.getKeyDown("down")) { game.camera.setPitch(-1); } if (inputManager.getKeyDown("left")) { game.camera.setYaw(-1); } if (inputManager.getKeyDown("right")) { game.camera.setYaw(1); } if (inputManager.getKeyDown("tab")) { if (isWireframeModeEnabled) { isWireframeModeEnabled = false; } else { isWireframeModeEnabled = true; } } } void Engine::drawGameObjects(const std::vector<GameObject*>& gameObjects) const { for (GameObject* model : gameObjects) { for (Script *script : model->scripts) { script->run(); } baseShader->editShaderWithMat4("view", game.camera.getView()); baseShader->editShaderWithMat4("perspective", glm::perspective(glm::radians(45.0f), 1.88791f, 0.1f, 100.0f)); model->draw(*baseShader); drawGameObjects(model->nestedGameObjects); } } void Engine::drawMeshSubClass(Mesh *mesh) { baseShader->editShaderWithMat4("view", game.camera.getView()); baseShader->editShaderWithMat4("perspective", glm::perspective(glm::radians(45.0f), 1.88791f, 0.1f, 100.0f)); mesh->draw(*baseShader); }
Engine.h
// // Created by kirut on 10/16/2023. // #ifndef PROJECTBAHAMUT_ENGINE_H #define PROJECTBAHAMUT_ENGINE_H #include <vector> #include <chrono> #include "Camera.h" #include "GameObject.h" #include "SDL.h" #include "imgui.h" #include "Level.h" #include "engineUI.h" #include "Game.h" #include "Projects.h" #include "InputManager.h" #include "Brush.h" class engineUI; static struct WindowSize { int height; int width; } windowSize; class Engine { public: SDL_DisplayMode displayMode; SDL_Window *window = nullptr; SDL_GLContext glContext = nullptr; SDL_Event event; WindowSize windowSize; Project project; Game game; bool isWireframeModeEnabled = false; InputManager &inputManager = InputManager::getInstance(); engineUI *ui; // Dear Imgui to be implemented Engine(Project &chosenProject); ~Engine(); void engineLoop(); private: Shader *baseShader; // make this so its made after opengl context is created int initRendering(int winHeight, int winWidth); // SDL and OpenGL void eventMonitor(); void KeyboardInput(); void drawMeshSubClass(Mesh *mesh); void drawGameObjects(const std::vector<GameObject*>& gameObjects) const; }; #endif //PROJECTBAHAMUT_ENGINE_H
engineUI.cpp
// // Created by kirut on 10/20/2023. // #include "engineUI.h" #include "Script.h" engineUI::engineUI(SDL_Window *window, SDL_GLContext &glContext) { IMGUI_CHECKVERSION(); ImGui::CreateContext(); ImGuiIO& io = ImGui::GetIO(); // Setup Platform/Renderer backends ImGui_ImplSDL2_InitForOpenGL(window, glContext); ImGui_ImplOpenGL3_Init(); // setup top down brush viewer loadUiIcons(); } engineUI::~engineUI() { ImGui_ImplOpenGL3_Shutdown(); ImGui_ImplSDL2_Shutdown(); ImGui::DestroyContext(); } void engineUI::renderUI(Game *game) { ImGui_ImplOpenGL3_NewFrame(); ImGui_ImplSDL2_NewFrame(); ImGui::NewFrame(); projectFileExplorer(); configureNextWindowPosSize(ImVec2(ImGui::GetIO().DisplaySize.x - 320, 0), ImVec2(320, 300)); ImGui::Begin("Game Object Editor", reinterpret_cast<bool *>(true), ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoTitleBar); ImGui::Text("Game Object Editor"); ImGui::SameLine(); if (ImGui::Button("Save Map")) { engine->game.level->save(); } ImGui::Separator(); if (ImGui::TreeNode("Map Geometry")) { for (Brush *brush : game->level->brushList) { if (brush->name.empty()) { if (ImGui::Button("##")) { selectedObject = nullptr; this->selectedBrush = brush; } } else { if (ImGui::Button(brush->name.c_str())) { selectedObject = nullptr; selectedBrush = brush; } } } ImGui::TreePop(); } if (ImGui::BeginPopupContextWindow()) { if (ImGui::MenuItem("Add Cube Brush")) { game->level->brushList.push_back(new Brush()); } ImGui::EndPopup(); } if (ImGui::TreeNode("Game Objects")) { drawGameObjectButton(game->level->gameObjects); ImGui::TreePop(); } if (selectedObject != nullptr) { objectEditWindow(selectedObject); } if (selectedBrush != nullptr) { brushEditWindow(selectedBrush); } ImGui::End(); ImGui::ShowDemoWindow(); ImGui::Render(); ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); } void engineUI::objectEditWindow(GameObject *gameObject) { configureNextWindowPosSize(ImVec2(ImGui::GetIO().DisplaySize.x - 320, 300), ImVec2(320, 300)); ImGui::Begin("Object Editor", reinterpret_cast<bool *>(true), ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize); ImGui::Text("This is the object editor window!"); gameObject->name = drawTextInput("Object Name", gameObject->name); drawVec3Input("Object Position", gameObject->transform.position); drawVec3Input("Object Rotation", gameObject->transform.rotation); drawVec3Input("Object Scale", gameObject->transform.scale); ImGui::Separator(); ImGui::Text("Attached Scripts:"); for (int i = 0; i < gameObject->scripts.size(); ++i) { ImGui::Text(gameObject->scripts[i]->path.filename().string().c_str()); ImGui::SameLine(); if (ImGui::Button("Delete")) { gameObject->scripts.erase(gameObject->scripts.begin() + i); } } ImGui::Button("Drop New Script Here", ImVec2(320, 50)); if (ImGui::BeginDragDropTarget()) { if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("DRAG_SCRIPT_FILE")) { ImGui::SetMouseCursor(ImGuiMouseCursor_NotAllowed); ImGui::BeginTooltip(); ImGui::Text("Drop Script here to add to game object"); ImGui::EndTooltip(); gameObject->scripts.push_back(new Script(static_cast<char*>(payload->Data), gameObject)); // turn the void pointer to string :) } ImGui::EndDragDropTarget(); } ImGui::End(); } void engineUI::projectFileExplorer() { configureNextWindowPosSize(ImVec2(0, 0), ImVec2(320, ImGui::GetIO().DisplaySize.y)); ImGui::Begin("Project Explorer", reinterpret_cast<bool *>(true), ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize); ImGui::Text("Project Explorer"); ImGui::Separator(); displayFileTree(engine->project.path, 0); ImGui::End(); } /* * Recursively goes through files from project root and make button for each * if directory then does function again */ void engineUI::displayFileTree(const std::string &path, int level) { std::string tabs(level, '\t'); if (level > 0) { tabs += "-"; } for (const auto &file: std::filesystem::directory_iterator(path)) { auto fileName = file.path().filename().string(); ImGui::Text(tabs.c_str()); // it would be nice if UNICODE CHARACTERS WORKED ImGui::SameLine(); if (file.is_directory()) { // find if directory is open in ui bool isOpen = openFolders.find(file.path().string()) != openFolders.end(); ImGui::Image((void *) (intptr_t) (isOpen ? icons["folderOpen"]->id : icons["folderClosed"]->id), ImVec2(20, 20), ImVec2(0, 1), ImVec2(1, 0)); ImGui::SameLine(); if (ImGui::Button(fileName.c_str())) { // path is added to open directories if not open if (!isOpen) { openFolders.insert(file.path().string()); } else { openFolders.erase(file.path().string()); } } if (isOpen) { displayFileTree(file.path().string(), level + 1); } } else { handleFileTypes(file); } } } void engineUI::handleFileTypes(const std::filesystem::directory_entry& file) { auto fileName = file.path().filename().string(); std::string fileType = fileName.substr(fileName.find('.') + 1, fileName.length()); if (fileType == "lua") { // some files have drag drop and some don't if (ImGui::Button(fileName.c_str())) { // do file action } if (ImGui::BeginDragDropSource(ImGuiDragDropFlags_None)) { ImGui::SetDragDropPayload("DRAG_SCRIPT_FILE", file.path().string().c_str(), file.path().string().capacity()); // i don't know why capacity works and not size ImGui::Text(fileName.c_str()); ImGui::EndDragDropSource(); } } else { // normal button if (ImGui::Button(fileName.c_str())) { // do file action if (fileType == "bem") { selectedObject = nullptr; engine->game.level = new Level(file.path().string()); } } } } void engineUI::loadUiIcons() { icons["folderClosed"] = new Texture(R"(Assets\icons\folder-solid.png)"); icons["folderOpen"] = new Texture(R"(Assets\icons\folder-open-regular.png)"); icons["object"] = new Texture(R"(Assets\icons\cube-solid.png)"); } void engineUI::configureNextWindowPosSize(ImVec2 position, ImVec2 size) { ImGui::SetNextWindowPos(position, ImGuiCond_Once); ImGui::SetNextWindowSize(size, ImGuiCond_Once); } void engineUI::drawVec3Input(const std::string &inputName, glm::vec3 &vector3) { ImGui::Separator(); ImGui::Text((inputName + ":").c_str()); glm::vec3 vec3Buffer = vector3; if (ImGui::DragFloat3(("##" + inputName).c_str(), glm::value_ptr(vector3), 0.005f)) { } } std::string engineUI::drawTextInput(const std::string &inputName, std::string &text) { ImGui::Separator(); ImGui::Text((inputName + ":").c_str()); char buffer[256]; strcpy(buffer, text.c_str()); if (ImGui::InputText(("##" + inputName).c_str(), buffer, 256)) { text = buffer; } return text; } void engineUI::drawGameObjectButton(std::vector<GameObject*> &gameObjects) { for (auto& object : gameObjects) { ImGui::Image((void*)(intptr_t)icons["object"]->id, ImVec2(20, 20), ImVec2(0, 1), ImVec2(1, 0)); ImGui::SameLine(); if (object->name.empty()) { if (ImGui::Button("##")) { selectedBrush = nullptr; this->selectedObject = object; } } else if (ImGui::TreeNode(object->name.c_str())) { this->selectedObject = object; selectedBrush = nullptr; drawGameObjectButton(selectedObject->nestedGameObjects); ImGui::TreePop(); } } } void engineUI::brushEditWindow(Brush *brush) { configureNextWindowPosSize(ImVec2(ImGui::GetIO().DisplaySize.x - 320, 300), ImVec2(320, 300)); ImGui::Begin("Object Editor", reinterpret_cast<bool *>(true), ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize); ImGui::Text("This is the object editor window!"); brush->name = drawTextInput("Object Name", brush->name); ImGui::Separator(); ImGui::Checkbox("Snapping enabled for this brush", &brush->isSnapEnabled); ImGui::Separator(); if (brush->isSnapEnabled) { ImGui::Text("Object Position:"); if (ImGui::DragFloat3("##Object Position", glm::value_ptr(brush->transform.position), 1.0f)) { } } else { drawVec3Input("Object Position", brush->transform.position); // normal drag } drawVec3Input("Object Rotation", brush->transform.rotation); ImGui::Separator(); if (brush->isSnapEnabled) { ImGui::Text("Object Scale:"); if (ImGui::DragFloat3("##Object Scale", glm::value_ptr(brush->transform.scale), 1.0f)) { } } else { drawVec3Input("Object Scale", brush->transform.scale); // normal drag } if (brush->isSnapEnabled) { brush->snapToWholeVerts(); } ImGui::End(); }
engineUI.h
// // Created by kirut on 10/20/2023. // #ifndef PROJECTBAHAMUT_ENGINEUI_H #define PROJECTBAHAMUT_ENGINEUI_H struct Game; #include "SDL_video.h" #include "imgui.h" #include "backends/imgui_impl_opengl3.h" #include "backends/imgui_impl_sdl2.h" #include "Engine.h" #include <filesystem> #include <unordered_set> class Project; class Engine; class engineUI { public: Engine *engine; engineUI(SDL_Window *window, SDL_GLContext &glContext); ~engineUI(); void renderUI(Game *game); private: std::map<std::string, Texture*> icons; GameObject *selectedObject = nullptr; Brush *selectedBrush = nullptr; std::unordered_set<std::string> openFolders; std::string *fileDragTemp; void objectEditWindow(GameObject *gameObject); void brushEditWindow(Brush *brush); void loadUiIcons(); static void configureNextWindowPosSize(ImVec2 position, ImVec2 size); static void drawVec3Input(const std::string& inputName, glm::vec3 &vector3); static std::string drawTextInput(const std::string& inputName, std::string &text); void drawGameObjectButton(std::vector<GameObject*> &gameObjects); void projectFileExplorer(); void displayFileTree(const std::string &path, int level); void handleFileTypes(const std::filesystem::directory_entry& file); void openFile(std::string path); }; #endif //PROJECTBAHAMUT_ENGINEUI_H
also bonus question would be if yaml would be the best way of saving maps to a file like how I am doing in level.cpp
// // Created by kirut on 10/18/2023. // #include "Level.h" Level::Level() = default; Level::Level(const std::string& levelFile) { path = levelFile; try { YAML::Node level = YAML::LoadFile(levelFile); // go for (auto &&object : level["Brushes"]) { brushList.push_back(addBrush(object)); } // go through game objects for (auto &&object : level["GameObjects"]) { gameObjects.push_back(addGameObject(object)); } } catch (const std::exception& e) { std::cerr << "Map Loading Error: " << e.what() << std::endl; } } void Level::save() const { std::ofstream file(path); file << "# -ProjectBahamut Map File-\n# You can edit the map through this file or the engine\n"; YAML::Node map; for (int i = 0; i < brushList.size(); ++i) { map["Brushes"][i]["name"] = brushList[i]->name; // position map["Brushes"][i]["position"][0] = gameObjects[i]->transform.position.x; map["Brushes"][i]["position"][1] = gameObjects[i]->transform.position.y; map["Brushes"][i]["position"][2] = gameObjects[i]->transform.position.z; // rotation glm::vec3 vecRotation = gameObjects[i]->transform.rotation; map["Brushes"][i]["rotation"][0] = vecRotation.x; map["Brushes"][i]["rotation"][1] = vecRotation.y; map["Brushes"][i]["rotation"][2] = vecRotation.z; // scale map["Brushes"][i]["scale"][0] = gameObjects[i]->transform.scale.x; map["Brushes"][i]["scale"][1] = gameObjects[i]->transform.scale.y; map["Brushes"][i]["scale"][2] = gameObjects[i]->transform.scale.z; } for (int i = 0; i < gameObjects.size(); i++) { map["GameObjects"][i]["name"] = gameObjects[i]->name; map["GameObjects"][i]["model"] = gameObjects[i]->modelPath; // position map["GameObjects"][i]["position"][0] = gameObjects[i]->transform.position.x; map["GameObjects"][i]["position"][1] = gameObjects[i]->transform.position.y; map["GameObjects"][i]["position"][2] = gameObjects[i]->transform.position.z; // rotation glm::vec3 vecRotation = gameObjects[i]->transform.rotation; map["GameObjects"][i]["rotation"][0] = vecRotation.x; map["GameObjects"][i]["rotation"][1] = vecRotation.y; map["GameObjects"][i]["rotation"][2] = vecRotation.z; // scale map["GameObjects"][i]["scale"][0] = gameObjects[i]->transform.scale.x; map["GameObjects"][i]["scale"][1] = gameObjects[i]->transform.scale.y; map["GameObjects"][i]["scale"][2] = gameObjects[i]->transform.scale.z; // any scripts for (int j = 0; j < gameObjects[i]->scripts.size(); ++j) { map["GameObjects"][i]["scripts"][j] = gameObjects[i]->scripts[j]->path.string(); } } file << map; file.close(); } GameObject* Level::addGameObject(const YAML::Node &object) { auto *newGameObject = new GameObject( object["name"].as<std::string>(), object["model"].as<std::string>(), glm::vec3(object["position"][0].as<float>(), object["position"][1].as<float>(), object["position"][2].as<float>()), glm::vec3(object["rotation"][0].as<float>(), object["rotation"][1].as<float>(), object["rotation"][2].as<float>()), glm::vec3(object["scale"][0].as<float>(), object["scale"][1].as<float>(), object["scale"][2].as<float>()) ); for (int i = 0; i < object["scripts"].size(); ++i) { newGameObject->scripts.push_back(new Script(object["scripts"][i].as<std::string>(), newGameObject)); } // for (int i = 0; i < object["nestedObjects"].size(); ++i) { // newGameObject->nestedGameObjects.push_back( // addGameObject(object["nestedObjects"][i])); // } return newGameObject; } Brush *Level::addBrush(const YAML::Node &object) { auto *newBrush = new Brush( object["name"].as<std::string>(), Transform(glm::vec3(object["position"][0].as<float>(), object["position"][1].as<float>(), object["position"][2].as<float>()), glm::vec3(object["rotation"][0].as<float>(), object["rotation"][1].as<float>(), object["rotation"][2].as<float>()), glm::vec3(object["scale"][0].as<float>(), object["scale"][1].as<float>(), object["scale"][2].as<float>())) ); return newBrush; }