From 21764d3f98a9001069981f5607ea502d2a3ecaa5 Mon Sep 17 00:00:00 2001 From: "Grayson Riffe (Laptop)" Date: Sat, 4 Dec 2021 02:17:33 -0600 Subject: [PATCH] Improved logger, added a debug timer, other small stuff, and wrote more user guide --- Game/Game.vcxproj | 2 + Game/src/Game.cpp | 2 + Game/src/MainState.cpp | 15 ++- NothinFancy/NothinFancy.vcxproj | 2 + NothinFancy/src/Application.cpp | 23 +++-- NothinFancy/src/Assets.cpp | 4 +- NothinFancy/src/AudioEngine.cpp | 19 +++- NothinFancy/src/Gamestate.cpp | 1 + NothinFancy/src/NFObject/Entity.cpp | 3 +- NothinFancy/src/NFObject/Sound.cpp | 3 +- NothinFancy/src/PhysicsEngine.cpp | 3 + NothinFancy/src/Renderer/Renderer.cpp | 95 +++++++++--------- NothinFancy/src/Utility.cpp | 60 +++++++++--- NothinFancy/src/include/NothinFancy.h | 7 +- NothinFancy/src/include/nf/Application.h | 1 - NothinFancy/src/include/nf/Entity.h | 3 +- NothinFancy/src/include/nf/Gamestate.h | 6 +- NothinFancy/src/include/nf/Utility.h | 59 +++++++++-- docs/Doxyfile | 4 +- docs/images/applifetime.png | Bin 0 -> 50023 bytes docs/images/blankapp.png | Bin 0 -> 6985 bytes docs/{ => images}/favicon.png | Bin docs/layout.xml | 9 +- docs/pages/2_tutorial.md | 120 ++++++++++++++++++++++- docs/theme.css | 6 +- 25 files changed, 326 insertions(+), 121 deletions(-) create mode 100644 docs/images/applifetime.png create mode 100644 docs/images/blankapp.png rename docs/{ => images}/favicon.png (100%) diff --git a/Game/Game.vcxproj b/Game/Game.vcxproj index 83eb4f0..f2386a9 100644 --- a/Game/Game.vcxproj +++ b/Game/Game.vcxproj @@ -61,6 +61,7 @@ true $(ProjectDir)src\include\;$(SolutionDir)NothinFancy\src\include\ $(IntDir)obj\ + true Console @@ -90,6 +91,7 @@ if exist "base.nfpack" (copy "base.nfpack" "$(OutDir)assets\") true $(ProjectDir)src\include\;$(SolutionDir)NothinFancy\src\include\ $(IntDir)obj\ + true Windows diff --git a/Game/src/Game.cpp b/Game/src/Game.cpp index b789e31..b9aacad 100644 --- a/Game/src/Game.cpp +++ b/Game/src/Game.cpp @@ -16,6 +16,8 @@ int main(int argc, char* argv[]) { app.setDefaultState("Main State"); app.run(); + + delete test; } return 0; diff --git a/Game/src/MainState.cpp b/Game/src/MainState.cpp index 26910a7..0d0c6a4 100644 --- a/Game/src/MainState.cpp +++ b/Game/src/MainState.cpp @@ -6,11 +6,9 @@ void MainState::onEnter() { camera->setType(currCamType); ap.load("example.nfpack"); - test.create(ap.get("2mats.obj"), nf::Entity::Type::DYNAMIC); - test.setPosition(nf::Vec3(0.0, 1.5, -5.0)); - plane.create(ap.get("env.obj"), nf::Entity::Type::MAP); + test.create(ap.get("2mats.obj"), nf::Vec3(0.0, 1.5, -5.0), nf::Entity::Type::DYNAMIC); + plane.create(ap.get("env.obj"), nf::Vec3(0.0, -20.0, 0.0), nf::Entity::Type::MAP); plane.setScale(20.0); - plane.setPosition(0.0, -20.0, 0.0); light.create(nf::Vec3(0.0, 20.0, 0.0), nf::Vec3(1.0, 1.0, 1.0)); light2.create(nf::Vec3(-10.0, 20.0, -10.0), nf::Vec3(1.0, 1.0, 1.0)); @@ -42,8 +40,7 @@ void MainState::onEnter() { for (int y = 0; y < 3; y++) { for (int z = 0; z < 3; z++) { entities.push_back(new nf::Entity); - entities.back()->create(nf::BaseAssets::cube, nf::Entity::Type::DYNAMIC); - entities.back()->setPosition(nf::Vec3(5.0 + x * 2.05, 1.0 + y * 2.05, -5.0 + z * 2.05)); + entities.back()->create(nf::BaseAssets::cube, nf::Vec3(5.0 + x * 2.05, 1.0 + y * 2.05, -5.0 + z * 2.05), nf::Entity::Type::DYNAMIC); } } } @@ -51,7 +48,8 @@ void MainState::onEnter() { grav = 2.0f; setGravity(grav); - amb = 0.5f; + amb = 0.1f; + setAmbientLight(amb); camera->setPosition(-20.0, 7.0, 0.0); camera->setRotation(85.0, -30.0); @@ -97,8 +95,7 @@ void MainState::update(float deltaTime) { if (camera->getType() == nf::Camera::Type::FIRST_PERSON && (app->isMouseClicked(NFI_LEFTMOUSE) || app->isMouseHeld(NFI_RIGHTMOUSE))) { entities.push_back(new nf::Entity); - entities.back()->create(nf::BaseAssets::sphere, nf::Entity::Type::DYNAMIC); - entities.back()->setPosition(camera->getPosition() + camera->getRotation() * 5.0); + entities.back()->create(nf::BaseAssets::sphere, camera->getPosition() + camera->getRotation() * 5.0, nf::Entity::Type::DYNAMIC); entities.back()->setVelocity(camera->getRotation() * 100.0f); entities.back()->setMass(1000.0f); } diff --git a/NothinFancy/NothinFancy.vcxproj b/NothinFancy/NothinFancy.vcxproj index c7d313b..c0d0ece 100644 --- a/NothinFancy/NothinFancy.vcxproj +++ b/NothinFancy/NothinFancy.vcxproj @@ -62,6 +62,7 @@ $(ProjectDir)src\include\;$(ProjectDir)dep\include\ $(IntDir)obj\ true + true Console @@ -94,6 +95,7 @@ $(ProjectDir)src\include\;$(ProjectDir)dep\include\ $(IntDir)obj\ true + true Console diff --git a/NothinFancy/src/Application.cpp b/NothinFancy/src/Application.cpp index df83f48..f9342fd 100644 --- a/NothinFancy/src/Application.cpp +++ b/NothinFancy/src/Application.cpp @@ -16,8 +16,8 @@ namespace nf { m_stateChange(false), m_stateChangeStarted(false) { - NFLog("Creating NF application"); - NFLog("Width: " + std::to_string(m_currentConfig.width) + ", Height: " + std::to_string(m_currentConfig.height) + ", Fullscreen: " + std::to_string(m_currentConfig.fullscreen) + ", Title: " + m_currentConfig.title); + NFLog("Constructing application"); + NFLog("Width: " + std::to_string(m_currentConfig.width) + ", Height: " + std::to_string(m_currentConfig.height) + ", Fullscreen: " + (m_currentConfig.fullscreen ? "true" : "false") + ", Title: " + m_currentConfig.title); if (getApp(true) != nullptr) NFError("Cannot create two NF Application objects!"); @@ -78,7 +78,9 @@ namespace nf { } void Application::run() { + NFLog("Running application..."); #ifdef _DEBUG + Debug::startTimer(); SetThreadDescription(GetCurrentThread(), L"Input Thread"); #endif if (m_defaultState.empty()) @@ -111,6 +113,9 @@ namespace nf { std::this_thread::sleep_for(std::chrono::milliseconds(5)); } mainThread.join(); +#ifdef _DEBUG + Debug::stopTimer(); +#endif } bool Application::hasCustomWindowIcon() { @@ -118,10 +123,12 @@ namespace nf { } void Application::changeState(const std::string& stateName) { + if (m_stateChange) return; if (m_states.find(stateName) == m_states.end()) NFError("State \"" + (std::string)stateName + (std::string)"\" doesn't exist!"); m_stateChange = true; m_nextState = m_states[stateName]; + NFLog("Changing to state \"" + stateName + (std::string)"\""); } Gamestate* Application::getCurrentState() { @@ -300,7 +307,6 @@ namespace nf { void Application::quit() { m_quit = true; - NFLog("Exiting NF application"); } void Application::runMainGameThread() { @@ -343,12 +349,11 @@ namespace nf { } } } - m_audio->stopAllSounds(); m_currentState->stop(); - delete sIntro; delete m_physics; delete m_audio; delete m_renderer; + delete sIntro; } void Application::doStateChange() { @@ -445,6 +450,7 @@ namespace nf { break; } case WM_CLOSE: { + NFLog("Exiting NF application"); DestroyWindow(hWnd); return 0; } @@ -456,12 +462,5 @@ namespace nf { return DefWindowProc(hWnd, uMsg, wParam, lParam); } - Application::~Application() { - for (std::pair state : m_states) { - Gamestate* curr = state.second; - delete curr; - } - } - Application* Application::currentApp = nullptr; } \ No newline at end of file diff --git a/NothinFancy/src/Assets.cpp b/NothinFancy/src/Assets.cpp index eb37763..21175c3 100644 --- a/NothinFancy/src/Assets.cpp +++ b/NothinFancy/src/Assets.cpp @@ -59,7 +59,6 @@ namespace nf { unsigned int cubemapCount = 0; unsigned int buttonCount = 0; while (packContents.size()) { - size_t startingPos = packContents.find_first_of("#NFASSET ") + 9; packContents = packContents.substr(9); size_t endAssetNamePos = packContents.find_first_of('\n'); std::string assetName = packContents.substr(0, endAssetNamePos); @@ -206,7 +205,6 @@ namespace nf { NFError("Could not find full button set in pack \"" + (std::string)packName + (std::string)"\"!"); while (packContentsOBJ.size()) { - size_t startingPos = packContentsOBJ.find_first_of("#NFASSET ") + 9; packContentsOBJ = packContentsOBJ.substr(9); size_t endAssetNamePos = packContentsOBJ.find_first_of('\n'); std::string assetName = packContentsOBJ.substr(0, endAssetNamePos); @@ -223,7 +221,6 @@ namespace nf { assetContents = packContentsOBJ; packContentsOBJ = ""; } - size_t assetSize = assetContents.size(); if (extension == "obj") { AModel* model = new AModel; @@ -236,6 +233,7 @@ namespace nf { } } assetContents = assetContents.substr(assetContents.find("\n") + 1); + size_t assetSize = assetContents.size(); model->data = new char[assetSize]; std::memcpy(model->data, &assetContents[0], assetSize); model->size = assetSize; diff --git a/NothinFancy/src/AudioEngine.cpp b/NothinFancy/src/AudioEngine.cpp index 1fae0cf..0444040 100644 --- a/NothinFancy/src/AudioEngine.cpp +++ b/NothinFancy/src/AudioEngine.cpp @@ -17,7 +17,7 @@ namespace nf { NFError("Could not initialize COM!"); hr = XAudio2Create(&m_engine); if (FAILED(hr)) - NFError("Could not initialize the audio engine!"); + NFError("Could not initialize audio engine!"); #ifdef _DEBUG XAUDIO2_DEBUG_CONFIGURATION debug = { 0 }; debug.TraceMask = XAUDIO2_LOG_ERRORS | XAUDIO2_LOG_WARNINGS; @@ -25,12 +25,16 @@ namespace nf { m_engine->SetDebugConfiguration(&debug, 0); #endif hr = m_engine->CreateMasteringVoice(&m_masterVoice); - if (hr == HRESULT_FROM_WIN32(ERROR_NOT_FOUND)) + if (hr == HRESULT_FROM_WIN32(ERROR_NOT_FOUND)) { m_isActive = false; - else if (SUCCEEDED(hr)) + NFLog("Audio engine not initialized since no audio devices found"); + } + else if (SUCCEEDED(hr)) { m_isActive = true; + NFLog("Initialized audio engine"); + } else - NFError("Could not initialize the audio engine!"); + NFError("Could not initialize audio engine!"); m_threadRunning = true; m_thread = std::thread(&AudioEngine::runAudioThread, this); } @@ -42,10 +46,11 @@ namespace nf { return false; else if (hr == S_OK) { m_isActive = true; + NFLog("Initialized audio engine"); return true; } else { - NFError("Could not initialize audio!"); + NFError("Could not initialize audio engine!"); return false; } } @@ -62,6 +67,9 @@ namespace nf { std::this_thread::sleep_for(std::chrono::milliseconds(100)); } + if (!m_isActive) + return; + DWORD cm; m_masterVoice->GetChannelMask(&cm); X3DAUDIO_HANDLE x3d; @@ -192,6 +200,7 @@ namespace nf { } AudioEngine::~AudioEngine() { + stopAllSounds(); m_threadRunning = false; m_thread.join(); m_engine->Release(); diff --git a/NothinFancy/src/Gamestate.cpp b/NothinFancy/src/Gamestate.cpp index 27d65af..579c783 100644 --- a/NothinFancy/src/Gamestate.cpp +++ b/NothinFancy/src/Gamestate.cpp @@ -24,6 +24,7 @@ namespace nf { if (physics) app->getPhysicsEngine()->newScene(); + NFTimerLoad; onEnter(); m_loading = false; diff --git a/NothinFancy/src/NFObject/Entity.cpp b/NothinFancy/src/NFObject/Entity.cpp index 93e90ad..b4b5f8c 100644 --- a/NothinFancy/src/NFObject/Entity.cpp +++ b/NothinFancy/src/NFObject/Entity.cpp @@ -22,10 +22,11 @@ namespace nf { m_member = true; } - void Entity::create(Asset* modelAsset, Type type) { + void Entity::create(Asset* modelAsset, const Vec3& position, Type type) { if (m_constructed) NFError("Entity already created!"); m_constructed = true; + setPosition(position); m_type = type; AModel* model; if ((model = dynamic_cast(modelAsset)) == nullptr) diff --git a/NothinFancy/src/NFObject/Sound.cpp b/NothinFancy/src/NFObject/Sound.cpp index 661eaaa..ef048cd 100644 --- a/NothinFancy/src/NFObject/Sound.cpp +++ b/NothinFancy/src/NFObject/Sound.cpp @@ -140,7 +140,7 @@ namespace nf { callbacks.close_func = v_close; callbacks.tell_func = v_tell; - int open = ov_open_callbacks(&memFile, &file, nullptr, 0, callbacks); + ov_open_callbacks(&memFile, &file, nullptr, 0, callbacks); char* buff = new char[65536 * 1000]; int stream = 0; @@ -164,7 +164,6 @@ namespace nf { } size_t Sound::loadWAV(std::string& data) { - unsigned int fileSize = *(unsigned int*)&data[4]; size_t fmtPos; if ((fmtPos = data.find("fmt")) == std::string::npos) NFError("WAV not of correct format!"); diff --git a/NothinFancy/src/PhysicsEngine.cpp b/NothinFancy/src/PhysicsEngine.cpp index 9a5d31b..af0be3d 100644 --- a/NothinFancy/src/PhysicsEngine.cpp +++ b/NothinFancy/src/PhysicsEngine.cpp @@ -30,6 +30,7 @@ namespace nf { m_stepSize(1.0f / 60.0f), m_accumulator(0.0) { + NFLog("Initializing physics engine..."); m_err = new PhysicsErrorCallback; m_foundation = PxCreateFoundation(PX_PHYSICS_VERSION, m_alloc, *m_err); if (!m_foundation) @@ -54,6 +55,8 @@ namespace nf { m_dispacher = PxDefaultCpuDispatcherCreate(threads); m_defaultMat = m_phy->createMaterial(0.5f, 0.5f, 0.0f); + + NFLog("Initialized physics engine"); } void PhysicsEngine::newScene() { diff --git a/NothinFancy/src/Renderer/Renderer.cpp b/NothinFancy/src/Renderer/Renderer.cpp index 28c79e4..9b6bc88 100644 --- a/NothinFancy/src/Renderer/Renderer.cpp +++ b/NothinFancy/src/Renderer/Renderer.cpp @@ -34,6 +34,7 @@ namespace nf { m_fadeText(true), m_fadeOutComplete(false) { + NFLog("Initializing renderer..."); m_hdc = GetDC(m_app->getWindow()); PIXELFORMATDESCRIPTOR pfd = { sizeof(PIXELFORMATDESCRIPTOR), @@ -123,6 +124,8 @@ namespace nf { m_quadVAO->finishBufferLayout(); m_quadIB = new IndexBuffer(quadIB, 6); m_loadingText.create("NFLoadingText", Vec2(0.025f, 0.044f), Vec3(0.7f)); + + NFLog("Initialized renderer"); } void Renderer::setFade(bool in, bool out, bool text) { @@ -165,59 +168,57 @@ namespace nf { glm::mat4 proj = glm::perspective(glm::radians(45.0f), (float)m_app->getConfig().width / (float)m_app->getConfig().height, 0.1f, 1000.0f); camera->update(m_gBufferShader, m_lightingShader, m_cubemapShader); - //First, fill the gBuffer with entities - m_gBufferShader->setUniform("proj", proj); - m_gBuffer->render(m_lGame, m_gBufferShader); + if (m_lGame.size()) { + //First, fill the gBuffer with entities + m_gBufferShader->setUniform("proj", proj); + m_gBuffer->render(m_lGame, m_gBufferShader); - //Light entities using the gBuffer - size_t lightsRemaining = m_lights.size(); - if (!lightsRemaining) { - m_quadVAO->bind(); - m_quadIB->bind(); - m_lightingShader->bind(); - m_gBuffer->bindTextures(m_lightingShader); - glDrawElements(GL_TRIANGLES, m_quadIB->getCount(), GL_UNSIGNED_INT, nullptr); + //Light entities using the gBuffer + size_t lightsRemaining = m_lights.size(); + unsigned int drawCount = 0; + do { + size_t currLightsDrawn; + if (lightsRemaining > m_texSlots) + currLightsDrawn = m_texSlots; + else + currLightsDrawn = lightsRemaining; + lightsRemaining -= currLightsDrawn; + m_lightingShader->setUniform("numberOfLights", (int)currLightsDrawn); + if (drawCount == 0) + m_lightingShader->setUniform("isContinued", false); + else { + m_lightingShader->setUniform("isContinued", true); + glBlendFunc(GL_ONE, GL_ONE); + glDepthFunc(GL_LEQUAL); + } + for (unsigned int i = 0; i < currLightsDrawn; i++) + m_lights[i]->bind(m_lightingShader, i); + renderShadowMaps(currLightsDrawn); + glBindFramebuffer(GL_FRAMEBUFFER, 0); + glViewport(0, 0, m_app->getConfig().width, m_app->getConfig().height); + m_quadVAO->bind(); + m_quadIB->bind(); + m_lightingShader->bind(); + m_gBuffer->bindTextures(m_lightingShader); +#ifdef _DEBUG + m_lightingShader->validate(); +#endif + glDrawElements(GL_TRIANGLES, m_quadIB->getCount(), GL_UNSIGNED_INT, nullptr); + m_lights.erase(m_lights.begin(), m_lights.begin() + currLightsDrawn); + drawCount++; + } while (lightsRemaining > 0); + m_lGame.clear(); + m_lights.clear(); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + glDepthFunc(GL_LESS); } - unsigned int drawCount = 0; - while (lightsRemaining > 0) { - size_t currLightsDrawn; - if (lightsRemaining > m_texSlots) - currLightsDrawn = m_texSlots; - else - currLightsDrawn = lightsRemaining; - lightsRemaining -= currLightsDrawn; - m_lightingShader->setUniform("numberOfLights", (int)currLightsDrawn); - if(drawCount == 0) - m_lightingShader->setUniform("isContinued", false); - else { - m_lightingShader->setUniform("isContinued", true); - glBlendFunc(GL_ONE, GL_ONE); - glDepthFunc(GL_LEQUAL); - } - for (unsigned int i = 0; i < currLightsDrawn; i++) - m_lights[i]->bind(m_lightingShader, i); - renderShadowMaps(currLightsDrawn); - glBindFramebuffer(GL_FRAMEBUFFER, 0); - glViewport(0, 0, m_app->getConfig().width, m_app->getConfig().height); - m_quadVAO->bind(); - m_quadIB->bind(); - m_lightingShader->bind(); - m_gBuffer->bindTextures(m_lightingShader); - glDrawElements(GL_TRIANGLES, m_quadIB->getCount(), GL_UNSIGNED_INT, nullptr); - m_lights.erase(m_lights.begin(), m_lights.begin() + currLightsDrawn); - drawCount++; - } - m_lGame.clear(); - m_lights.clear(); - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - glDepthFunc(GL_LESS); //Draw the cubemap if one is currently set if (m_cubemap != nullptr) { m_cubemapShader->setUniform("proj", proj); m_cubemap->render(m_cubemapShader); + m_cubemap = nullptr; } - m_cubemap = nullptr; //Draw UI elements glDisable(GL_DEPTH_TEST); @@ -297,6 +298,7 @@ namespace nf { } void Renderer::loadBaseAssets() { + NFLog("Loading base assets..."); m_baseAP.load("base.nfpack"); const char* gBufferVertex = m_baseAP.get("gBufferVertex.shader")->data; const char* gBufferFragment = m_baseAP.get("gBufferFragment.shader")->data; @@ -334,6 +336,8 @@ namespace nf { BaseAssets::cubemap = (ACubemap*)m_baseAP.get("default.cm"); BaseAssets::font = (AFont*)m_baseAP.get("default.ttf"); BaseAssets::button = (AButton*)m_baseAP.get("default.button"); + + NFLog("Base assets loaded"); } void Renderer::createShadowMaps() { @@ -371,7 +375,6 @@ namespace nf { glm::mat4 lightView; glm::mat4 lightSpaceMat; bool directionalRendered = false; - unsigned int directionalSlot = 0; //TODO: Test this glBindFramebuffer(GL_FRAMEBUFFER, m_shadowMapFBO); for (unsigned int i = 0; i < count; i++) { Light::Type type = m_lights[i]->getType(); diff --git a/NothinFancy/src/Utility.cpp b/NothinFancy/src/Utility.cpp index aa8a7f9..bb121a0 100644 --- a/NothinFancy/src/Utility.cpp +++ b/NothinFancy/src/Utility.cpp @@ -19,47 +19,79 @@ namespace nf { #ifdef _DEBUG NFDEBUGINIT; + void Debug::startTimer() { + m_timerStarted = true; + m_initTime = std::chrono::steady_clock::now(); + } + + void Debug::stopTimer() { + m_timerStarted = false; + } + void Debug::LogImp(const char* in) { - std::chrono::duration time = getCurrentTime(); - std::printf("[%.4f] NF Log: %s\n", time.count(), in); + if(m_timerStarted) + printCurrentTime(); + std::printf("NF Log: %s\n", in); } void Debug::LogImp(const std::string& in) { - std::chrono::duration time = getCurrentTime(); - std::printf("[%.4f] NF Log: ", time.count()); + if (m_timerStarted) + printCurrentTime(); + std::printf("NF Log: "); std::cout << in << "\n"; } void Debug::LogImp(int in) { - std::chrono::duration time = getCurrentTime(); - std::printf("[%.4f] NF Log: %i\n", time.count(), in); + if (m_timerStarted) + printCurrentTime(); + std::printf("NF Log: %i\n", in); } void Debug::LogImp(float in) { - std::chrono::duration time = getCurrentTime(); - std::printf("[%.4f] NF Log: %.4f\n", time.count(), in); + if (m_timerStarted) + printCurrentTime(); + std::printf("NF Log: %.4f\n", in); } //TODO: Test every Error in release mode void Debug::ErrorImp(const char* in, const char* filename, int line) { - std::chrono::duration time = getCurrentTime(); + if (m_timerStarted) + printCurrentTime(); static HANDLE cmd = GetStdHandle(STD_OUTPUT_HANDLE); SetConsoleTextAttribute(cmd, FOREGROUND_RED); - std::printf("[%.4f] NF Error (%s, %i): %s\n", time.count(), filename, line, in); + std::printf("NF Error (%s, %i): %s\n", filename, line, in); SetConsoleTextAttribute(cmd, 7); } void Debug::ErrorImp(const std::string& in, const char* filename, int line) { - std::chrono::duration time = getCurrentTime(); + if (m_timerStarted) + printCurrentTime(); static HANDLE cmd = GetStdHandle(STD_OUTPUT_HANDLE); SetConsoleTextAttribute(cmd, FOREGROUND_RED); - std::printf("[%.4f] NF Error (%s, %i): ", time.count(), filename, line); + std::printf("NF Error (%s, %i): ", filename, line); std::cout << in << "\n"; SetConsoleTextAttribute(cmd, 7); } - std::chrono::duration Debug::getCurrentTime() { + void Debug::printCurrentTime() { std::chrono::steady_clock::time_point now = std::chrono::high_resolution_clock::now(); - return now - m_initTime; + std::chrono::duration dur = now - m_initTime; + std::printf("[%.4f] ", dur.count()); + } + + Timer::Timer(const std::string& function, bool onEnter) { + m_funcName = function; + m_initTime = std::chrono::steady_clock::now(); + m_loading = onEnter; + if (!m_loading) + NFLog("Started timing \"" + m_funcName + (std::string)"\""); + } + + Timer::~Timer() { + std::chrono::duration dur = std::chrono::steady_clock::now() - m_initTime; + if (!m_loading) + NFLog("\"" + m_funcName + (std::string)"\" took " + std::to_string(dur.count()) + (std::string)" seconds."); + else + NFLog("Loading took " + std::to_string(dur.count()) + (std::string)" seconds."); } #endif diff --git a/NothinFancy/src/include/NothinFancy.h b/NothinFancy/src/include/NothinFancy.h index 2759301..73cb3a2 100644 --- a/NothinFancy/src/include/NothinFancy.h +++ b/NothinFancy/src/include/NothinFancy.h @@ -1,11 +1,8 @@ +//This is the main header for the entire engine. #pragma once -//TODO: Rework this file to only contain functions the frontend will need to access -//Maybe a implementation define here? - -//TODO: Prevent including other headers other than this one #ifndef NFIMPL -#define NFIMPL 1 +#define NFIMPL #endif #include "nf/Config.h" diff --git a/NothinFancy/src/include/nf/Application.h b/NothinFancy/src/include/nf/Application.h index 95372e0..4685552 100644 --- a/NothinFancy/src/include/nf/Application.h +++ b/NothinFancy/src/include/nf/Application.h @@ -177,7 +177,6 @@ namespace nf { void getMouseDiff(int& x, int& y); static Application* getApp(bool first = false); #endif - ~Application(); private: void registerWindowClass(); RECT getWindowRect() const; diff --git a/NothinFancy/src/include/nf/Entity.h b/NothinFancy/src/include/nf/Entity.h index db2edb4..5e50894 100644 --- a/NothinFancy/src/include/nf/Entity.h +++ b/NothinFancy/src/include/nf/Entity.h @@ -53,6 +53,7 @@ namespace nf { /** * @brief Creates an entity * @param modelAsset A model Asset pointer + * @param position Initial position vector * @param type Type of entity; Defaults to Type::STATIC * * This function will initialize an entity by loading its associated model from @@ -61,7 +62,7 @@ namespace nf { * @warning Calling this function twice before the state exits will result in an * error. See @ref isConstructed. */ - void create(Asset* modelAsset, Type type = Type::STATIC); + void create(Asset* modelAsset, const Vec3& position, Type type = Type::STATIC); /** * @brief Queries whether or not the entity has been created * @return If the entity has been created diff --git a/NothinFancy/src/include/nf/Gamestate.h b/NothinFancy/src/include/nf/Gamestate.h index 77acefe..35f02db 100644 --- a/NothinFancy/src/include/nf/Gamestate.h +++ b/NothinFancy/src/include/nf/Gamestate.h @@ -14,10 +14,10 @@ namespace nf { class Texture; /** - * @brief A state NF can be in that includes a collection of objects and user-defined + * @brief An engine state that includes a world, objects, and user-defined * behavior * - * Every user-defined state inherits from this class. + * Every state inherits from this class. */ class Gamestate { public: @@ -37,7 +37,7 @@ namespace nf { * @brief Update function * @param deltaTime Amount of time the previous frame took to produce in seconds * - * This function is called every frame. It is called before render. + * This function is called every frame. It is called before @ref render. * * The deltaTime parameter's purpose is to create non-frame-dependant gameplay. This * number should be multiplied with user numbers likes velocities. Doing this will diff --git a/NothinFancy/src/include/nf/Utility.h b/NothinFancy/src/include/nf/Utility.h index af9a7e3..d54f743 100644 --- a/NothinFancy/src/include/nf/Utility.h +++ b/NothinFancy/src/include/nf/Utility.h @@ -7,7 +7,7 @@ /** * @brief Nothin' Fancy namespace * - * Every class and struct lives inside of this namespace + * Every class and struct lives inside of this namespace. * * It could be useful to `using` this namespace: * @@ -20,7 +20,15 @@ namespace nf { //Strips __FILE__ down to only the name of the file #define __FILENAME__ strrchr(__FILE__, '\\') + 1 //Initializes static variables needed for debugging -#define NFDEBUGINIT std::chrono::steady_clock::time_point Debug::m_initTime = std::chrono::high_resolution_clock::now(); +#define NFDEBUGINIT std::chrono::steady_clock::time_point Debug::m_initTime = std::chrono::high_resolution_clock::now(); \ +bool Debug::m_timerStarted = false +/** +* @defgroup macros Macros +* +* Macros to aid in debugging and developing with NF +* +* @{ +*/ /** * Pauses the engine for a set amount of seconds */ @@ -43,11 +51,33 @@ namespace nf { */ #define NFError(x) {nf::Debug::ErrorImp(x,__FILENAME__, __LINE__);\ __debugbreak();} - /** - * @brief Handles NFLog and NFError calls - */ +/** +* A timer useful for timing functions +* +* To time a function, place this macro at the beginning of it: +* +* ~~~ +* void myFunc() { +* NFTimer; +* //Rest of function to be timed... +* } //Prints here +* ~~~ +* +* The result will be logged when the scope it's declared in ends. +*/ +#define NFTimer nf::Timer _nfTimer(__func__) +/** +* @} +*/ +#ifndef NFIMPL +#define NFTimerLoad nf::Timer _nfTimer(__func__, true); +#endif + class Debug { public: + static void startTimer(); + static void stopTimer(); + static void LogImp(const char* in); static void LogImp(const std::string& in); static void LogImp(int in); @@ -57,8 +87,20 @@ __debugbreak();} [[noreturn]] static void ErrorImp(const std::string& in, const char* filename, int line); private: + static void printCurrentTime(); static std::chrono::steady_clock::time_point m_initTime; - static std::chrono::duration getCurrentTime(); + static bool m_timerStarted; + }; + + class Timer { + public: + Timer(const std::string& function, bool onEnter = false); + + ~Timer(); + private: + std::chrono::steady_clock::time_point m_initTime; + bool m_loading; + std::string m_funcName; }; #else #define NFDEBUGINIT @@ -67,6 +109,8 @@ __debugbreak();} #define NFLog(x) #define NFError(x) {MessageBox(FindWindow(L"NFClass", NULL), toWide(x).data(), L"NF Engine Error", MB_OK | MB_ICONERROR);\ std::exit(-1);} +#define NFTimer +#define NFTimerLoad #endif /** @@ -364,10 +408,11 @@ std::exit(-1);} float w; }; +#ifndef NFIMPL const std::wstring toWide(const char* in); const std::wstring toWide(const std::string& in); - Vec4 degToQuat(const Vec3& in); +#endif /** * @brief Writes a stream of bytes as as std::string into a file in a specified location diff --git a/docs/Doxyfile b/docs/Doxyfile index 2de5e61..8a15dc7 100644 --- a/docs/Doxyfile +++ b/docs/Doxyfile @@ -169,7 +169,7 @@ HTML_HEADER = header.html HTML_FOOTER = footer.html HTML_STYLESHEET = HTML_EXTRA_STYLESHEET = theme.css -HTML_EXTRA_FILES = favicon.png +HTML_EXTRA_FILES = images/favicon.png HTML_COLORSTYLE_HUE = 30 HTML_COLORSTYLE_SAT = 100 HTML_COLORSTYLE_GAMMA = 80 @@ -199,7 +199,7 @@ QHP_SECT_FILTER_ATTRS = QHG_LOCATION = GENERATE_ECLIPSEHELP = NO ECLIPSE_DOC_ID = org.doxygen.Project -DISABLE_INDEX = NO +DISABLE_INDEX = YES GENERATE_TREEVIEW = YES FULL_SIDEBAR = NO ENUM_VALUES_PER_LINE = 4 diff --git a/docs/images/applifetime.png b/docs/images/applifetime.png new file mode 100644 index 0000000000000000000000000000000000000000..a405b2365bddd37c033091a63fc241cb9e65d46e GIT binary patch literal 50023 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ϡ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 - + - - + + + @@ -35,7 +36,7 @@ - + diff --git a/docs/pages/2_tutorial.md b/docs/pages/2_tutorial.md index 7a377ad..09510db 100644 --- a/docs/pages/2_tutorial.md +++ b/docs/pages/2_tutorial.md @@ -6,13 +6,127 @@ This tutorial aims to teach the basics of the engine and how to use it. First, follow the steps on the @ref install page. Once the template MSVC project is setup, you can begin here. -@section nfArch NF Engine Architecture +@section nfArch Engine Architecture +An NF app is made up of a set of states represented by the nf::Gamestate class. When +an nf::Application is running, it is either running a state or loading one. Below is an +image that describes what the main thread of an NF app would be typically doing in +the program's lifetime. +@image html applifetime.png "The lifetime of a typical NF app" width=20% + +Using the engine's architecture, you might not even write any functions that are called +from `main`. Most of the code that programs the engine's behavior should be called in +your state's [update function](@ref nf::Gamestate::update). + +To allow a translate unit to use the engine, you must include `NothinFancy.h`. This +header contains every class and function. @section createConfig Creating a Config -@todo The tutorial page +The first step to creating an app is creating an nf::Config. A config describes how the +engine should display on the screen. nf::Config has these fields: + +- `width` - The width of the window if `fullscreen` is set to `false` +- `height` - The height of the window if `fullscreen` is set to `false` +- `fullscreen` - `true` sets the display to the size of the monitor the app is +- `title` - The title of the window shown on the caption bar and taskbar +opened on + +To create a 1280 by 720 window with a title of "NF Example", you would write: + +~~~cpp +nf::Config conf; +conf.width = 1280; +conf.height = 720; +conf.fullscreen = false; +conf.title = "NF Example"; + +//Or... + +nf::Config conf2{ 1280, 720, false, "NF Example" }; +~~~ + +We then pass this config to an nf::Application + +@section createApp Creating and Configuring an Application + +The nf::Application class represents an instance of the engine. This is the point where +you will attach your states and run the engine. + +@note In a program and on a single machine, there can only be one single instance of +this class at a time. Attempting to create mulitple will result in an error. + +The constructor takes in the nf::Config we previously created: + +~~~cpp +nf::Application app(conf); +~~~ + +Constructing an application doesn't do much. It merely allows you to access the member +functions to setup your application. + +Here are some functions you might want to call at this point: + +- [setWindowIcon](@ref nf::Application::setWindowIcon) - Sets the icon of the window +- [setWindowCursor](@ref nf::Application::setWindowCursor) - Sets the cursor's image +when it is visible and inside the window + +And here are the functions you **must** call before an app can run: + +- [addState](@ref nf::Application::addState) - Adds a state to an app so that it can +access it later by a user-defined string identifier +- [setDefaultState](@ref nf::Application::setDefaultState) - Sets the state to load +after the logo state exits + +Once these functions have been called, the app can be run: + +~~~cpp +CustomGamestate* customState = new CustomGamestate; //Inherits nf::Gamestate +app.addState(customState, "State 1"); //"State One" is this state's identifier. +app.setDefaultState("State 1"); //Will error without this +app.run(); //Blocks until exit +~~~ + +@section createGamestate Creating a Gamestate + +To create a gamestate, you must create a class that publicly inherits nf::Gamestate +and overrides its four virtual functions: + +- [onEnter](@ref nf::Gamestate::onEnter) - Called when the state is loading; Where member +objects are initialized +- [update](@ref nf::Gamestate::update) - Called every frame after loading is complete; +Where custom behavior is defined +- [render](@ref nf::Gamestate::render) - Called after update; Selects what to render +- [onExit](@ref nf::Gamestate::onExit) - Called when the state is unloaded + +A gamestate class might look something like this: + +~~~cpp +class CustomGamestate : public nf::Gamestate { +public: + void onEnter() override; + void update(float deltaTime) override; //Note the parameter + void render(nf::Renderer& renderer) override; + void onExit() override; + + //Implementations somewhere else... + +private: + //Member objects here... +}; +~~~ + +Once an app has been setup and run with an empty state, you should be met with a black screen +after the logo state: + +@image html blankapp.png "A blank gamestate" width=50% + +Congratulations! You now have a basic NF app running. Now we can add objects to our world. + +@section createEntities Adding 3D Objects + + @section createUI Creating a UI @@ -20,4 +134,4 @@ you can begin here. @todo Lighting page? -@section packaging Packaging Your Game \ No newline at end of file +@section packaging Packaging Your App \ No newline at end of file diff --git a/docs/theme.css b/docs/theme.css index 35e87cd..59fbbfb 100644 --- a/docs/theme.css +++ b/docs/theme.css @@ -314,7 +314,7 @@ pre.fragment { } div.fragment { - padding: 0 0 1px 0; /*Fixed: last line underline overlap border*/ + padding: 5px 0 5px 5px; /*Fixed: last line underline overlap border*/ margin: 4px 8px 4px 2px; background-color: #FDFCFB; border: 1px solid #E5D5C4; @@ -322,9 +322,9 @@ div.fragment { div.line { font-family: monospace, fixed; - font-size: 13px; + font-size: 15px; min-height: 13px; - line-height: 1.0; + line-height: 1.1; text-wrap: unrestricted; white-space: -moz-pre-wrap; /* Moz */ white-space: -pre-wrap; /* Opera 4-6 */