From 2ab07e6d6cd0cab80abc54bb4235966eba80ad90 Mon Sep 17 00:00:00 2001
From: Joseph Walton-Rivers <joseph@walton-rivers.uk>
Date: Sun, 13 Mar 2022 15:48:03 +0000
Subject: [PATCH] cleaner game loop code

---
 demo/main.cpp                | 186 ++++++++++++++++++++++-------------
 fggl/CMakeLists.txt          |   1 +
 fggl/app.cpp                 |  39 ++++++++
 include/fggl/app.hpp         | 126 ++++++++++++++++++++++++
 include/fggl/fggl.hpp        |  30 ++++++
 include/fggl/util/states.hpp |  76 ++++++++++++++
 6 files changed, 387 insertions(+), 71 deletions(-)
 create mode 100644 fggl/app.cpp
 create mode 100644 include/fggl/app.hpp
 create mode 100644 include/fggl/util/states.hpp

diff --git a/demo/main.cpp b/demo/main.cpp
index 188813c..2a04b5f 100644
--- a/demo/main.cpp
+++ b/demo/main.cpp
@@ -1,7 +1,10 @@
+#include <fggl/ecs3/types.hpp>
 #include <filesystem>
 #include <iostream>
-
+#include <memory>
 #include <utility>
+
+#include <fggl/app.hpp>
 #include <fggl/gfx/window.hpp>
 #include <fggl/gfx/camera.hpp>
 #include <fggl/input/camera_input.h>
@@ -24,6 +27,8 @@
 
 constexpr bool showNormals = false;
 
+std::shared_ptr<fggl::gfx::ecsOpenGLModule> glModule;
+
 // prototype of resource discovery
 void discover(const std::filesystem::path& base) {
 
@@ -93,12 +98,19 @@ void placeObject(fggl::ecs3::World& world, fggl::ecs::entity_t parent, fggl::ecs
     result->origin( targetPos );
 }
 
-class MenuScene : public fggl::scenes::Scene {
+class MenuScene : public fggl::scenes::Scene, public fggl::AppState {
 
 public:
-    explicit MenuScene(InputManager inputs) : m_inputs(std::move(inputs)) {}
+    explicit MenuScene(fggl::App& app) : fggl::AppState(app), m_inputs(nullptr) {}
     ~MenuScene() override = default;
 
+    void activate() override {
+        auto& locator = fggl::util::ServiceLocator::instance();
+        m_inputs = locator.providePtr<fggl::input::Input>();
+
+        setup();
+    }
+
     void setup() override {
     }
 
@@ -107,6 +119,10 @@ public:
     }
 
     void update() override {
+        if ( !m_inputs ) {
+            return;
+        }
+
         bool leftMouse = m_inputs->mouse.button(fggl::input::MouseButton::LEFT);
         if (leftMouse) {
             auto scenes = fggl::util::ServiceLocator::instance().providePtr<fggl::scenes::SceneManager>();
@@ -123,25 +139,39 @@ private:
 
 };
 
-class GameScene : public fggl::scenes::Scene {
+class GameScene : public fggl::scenes::Scene, public fggl::AppState {
 public:
-    explicit GameScene(fggl::ecs3::World& world, InputManager inputs) : m_world(world), m_inputs(std::move(inputs)) { };
+    explicit GameScene(fggl::App& app) : fggl::AppState(app), m_world(nullptr), m_sceneTime(nullptr), m_inputs(nullptr) { };
     ~GameScene() override = default;
 
+    void activate() override {
+        auto& locator = fggl::util::ServiceLocator::instance();
+        m_inputs = locator.providePtr<fggl::input::Input>();
+
+        auto types = locator.providePtr<fggl::ecs3::TypeRegistry>();
+        m_world = std::make_unique<fggl::ecs3::World>(*types);
+
+        m_sceneTime = std::make_unique<fggl::util::Timer>();
+        m_sceneTime->frequency( glfwGetTimerFrequency() );
+        m_sceneTime->setup( glfwGetTimerValue() );
+
+        setup();
+    }
+
     void setup() override {
-        auto types = m_world.types();
+        auto types = m_world->types();
 
         // create camera using strings
         {
-            auto prototype = m_world.create(false);
-            m_world.add(prototype, types.find(fggl::math::Transform::name));
-            m_world.add(prototype, types.find(fggl::gfx::Camera::name));
-            m_world.add(prototype, types.find(fggl::input::FreeCamKeys::name));
+            auto prototype = m_world->create(false);
+            m_world->add(prototype, types.find(fggl::math::Transform::name));
+            m_world->add(prototype, types.find(fggl::gfx::Camera::name));
+            m_world->add(prototype, types.find(fggl::input::FreeCamKeys::name));
 
-            auto camTf = m_world.get<fggl::math::Transform>(prototype);
+            auto camTf = m_world->get<fggl::math::Transform>(prototype);
             camTf->origin( glm::vec3(10.0f, 3.0f, 10.0f) );
 
-            auto cameraKeys = m_world.get<fggl::input::FreeCamKeys>(prototype);
+            auto cameraKeys = m_world->get<fggl::input::FreeCamKeys>(prototype);
             cameraKeys->forward = glfwGetKeyScancode(GLFW_KEY_W);
             cameraKeys->backward = glfwGetKeyScancode(GLFW_KEY_S);
             cameraKeys->left = glfwGetKeyScancode(GLFW_KEY_A);
@@ -152,10 +182,10 @@ public:
 
         fggl::ecs3::entity_t terrain;
         {
-            terrain = m_world.create(false);
-            m_world.add(terrain, types.find(fggl::math::Transform::name));
+            terrain = m_world->create(false);
+            m_world->add(terrain, types.find(fggl::math::Transform::name));
 
-            auto camTf = m_world.get<fggl::math::Transform>(terrain);
+            auto camTf = m_world->get<fggl::math::Transform>(terrain);
             camTf->origin( glm::vec3(0.0f, 0.0f, 0.0f) );
 
             //auto terrainData = m_world.get<fggl::data::HeightMap>(terrain);
@@ -171,14 +201,14 @@ public:
                     terrainData.heightValues[x * 255 +y] = (float)noise;
                 }
             }
-            m_world.set<fggl::data::HeightMap>(terrain, &terrainData);
+            m_world->set<fggl::data::HeightMap>(terrain, &terrainData);
         }
 
         // create foundation object
         fggl::ecs3::entity_t foundation;
         {
-            foundation = m_world.create(true);
-            m_world.add(foundation, types.find(fggl::math::Transform::name));
+            foundation = m_world->create(true);
+            m_world->add(foundation, types.find(fggl::math::Transform::name));
 
             // plot rendering
             fggl::data::Mesh mesh;
@@ -188,14 +218,14 @@ public:
             // add mesh as a component
             constexpr char shader[] = "phong";
             fggl::gfx::StaticMesh staticMesh{mesh, shader};
-            m_world.set<fggl::gfx::StaticMesh>(foundation, &staticMesh);
+            m_world->set<fggl::gfx::StaticMesh>(foundation, &staticMesh);
         }
 
         // create building prototype
         fggl::ecs3::entity_t bunker;
         {
-            bunker = m_world.create(true);
-            m_world.add(bunker, types.find(fggl::math::Transform::name));
+            bunker = m_world->create(true);
+            m_world->add(bunker, types.find(fggl::math::Transform::name));
 
             // mesh
             int nSections = 2;
@@ -221,12 +251,12 @@ public:
             mesh.removeDups();
 
             fggl::gfx::StaticMesh staticMesh{mesh, shader};
-            m_world.set<fggl::gfx::StaticMesh>(bunker, &staticMesh);
+            m_world->set<fggl::gfx::StaticMesh>(bunker, &staticMesh);
         }
 
         for (int i=0; i<3; ++i) {
             glm::vec3 location(i * 6.5f + 1.0f, 0.0f, -7.0f);
-            placeObject(m_world, terrain, foundation, location);
+            placeObject(*m_world, terrain, foundation, location);
         }
 
         int nCubes = 3;
@@ -234,16 +264,20 @@ public:
             glm::vec3 location;
             location.x = i * 6.f + 1.0f;
             location.z = -5.0f + 1.0f;
-            placeObject(m_world, terrain, bunker, location);
+            placeObject(*m_world, terrain, bunker, location);
         }
     }
 
+    void deactivate() override {
+        cleanup();
+    }
+
     void cleanup() override {
 
     }
 
     void update() override {
-        process_camera(m_world, m_inputs);
+        process_camera(*m_world, m_inputs);
 
         // JWR - this doesn't really seem like it belongs in the game scene...
         if ( m_inputs->keyboard.pressed(glfwGetKeyScancode(GLFW_KEY_F10)) ) {
@@ -254,18 +288,19 @@ public:
 
     void render() override {
         debugInspector();
+        fggl::gfx::renderMeshes(glModule, *m_world, m_sceneTime->delta());
     }
 
     // entity inspector
     void debugInspector() {
-        auto types = m_world.types();
+        auto types = m_world->types();
 
         ImGui::Begin("Entities");
-        auto entityItr = m_world.all();
+        auto entityItr = m_world->all();
         for (auto& entity : entityItr) {
             std::string label = "entity-" + std::to_string(entity);
             if ( ImGui::TreeNode(label.c_str()) ){
-                auto entComp = m_world.getComponents(entity);
+                auto entComp = m_world->getComponents(entity);
                 for ( auto comp : entComp ){
                     auto meta = types.meta(comp);
                     ImGui::Text("%s (%d) - %lu bytes", meta->name(), comp, meta->size());
@@ -277,7 +312,8 @@ public:
     };
 
 private:
-    fggl::ecs3::World& m_world;
+    std::unique_ptr<fggl::ecs3::World> m_world;
+    std::unique_ptr<fggl::util::Timer> m_sceneTime;
     InputManager m_inputs;
 };
 
@@ -326,16 +362,23 @@ void gamepadDebug(bool* visible) {
         ImGui::End();
 }
 
-int main(int argc, char* argv[]) {
+int main(int argc, const char* argv[]) {
+    fggl::App app( "fggl-demo" );
+
+    app.add_state<MenuScene>("menu");
+    app.add_state<GameScene>("game");
+
     auto& locator = fggl::util::ServiceLocator::instance();
 
     // setup ECS Types
-    fggl::ecs3::TypeRegistry types;
-    types.make<fggl::math::Transform>();
-    fggl::ecs3::ModuleManager modules(types);
+    auto types = std::make_shared< fggl::ecs3::TypeRegistry >();
+    locator.supply<fggl::ecs3::TypeRegistry>( types );
+
+    types->make<fggl::math::Transform>();
+    fggl::ecs3::ModuleManager modules(*types);
 
     // setup ECS
-    fggl::ecs3::World ecs(types);
+    //fggl::ecs3::World ecs(types);
 
     // input management
     auto inputs = std::make_shared<fggl::input::Input>();
@@ -352,7 +395,7 @@ int main(int argc, char* argv[]) {
 	//discover( storage.resolvePath(fggl::data::Data, "../../packs") );
 
 	// Opengl APIs
-    auto glModule = modules.load<fggl::gfx::ecsOpenGLModule>(window, storage);
+    glModule = modules.load<fggl::gfx::ecsOpenGLModule>(window, storage);
 	fggl::gfx::loadPipeline(glModule, "unlit", false);
 	fggl::gfx::loadPipeline(glModule, "phong", false);
 	fggl::gfx::loadPipeline(glModule, "normals", false);
@@ -369,41 +412,42 @@ int main(int argc, char* argv[]) {
     // Scene management
     auto scenes = std::make_shared<fggl::scenes::SceneManager>();
     locator.supply<fggl::scenes::SceneManager>(scenes);
-    scenes->create("main_menu", std::make_shared<MenuScene>(inputs));
-    scenes->create("game", std::make_shared<GameScene>(ecs, inputs));
-    scenes->activate("main_menu");
-
-    // Main game/event loop
-	fggl::util::Timer time{};
-	time.frequency( glfwGetTimerFrequency() );
-	time.setup( glfwGetTimerValue() );
-
-	while( !window->closeRequested() ) {
-		//
-		// Setup setup
-		//
-		time.tick( glfwGetTimerValue() );
-		inputs->frame( time.delta() );
-
-		glfwModule->context.pollEvents();
-		debug->frameStart();
-
-		//
-		// update step
-		//
-        scenes->update();
-
-        // render the scene
-        window->activate();
-        glModule->ogl.clear();
-
-        // allow the scene to do stuff, then actually render
-        scenes->render();
-        fggl::gfx::renderMeshes(glModule, ecs, time.delta());
-
-        debug->draw();
-        window->swap();
-    }
+    //scenes->create("main_menu", std::make_shared<MenuScene>(inputs));
+    //scenes->create("game", std::make_shared<GameScene>(ecs, inputs));
+    //scenes->activate("main_menu");
+
+    /*
+        // Main game/event loop
+        fggl::util::Timer time{};
+        time.frequency( glfwGetTimerFrequency() );
+        time.setup( glfwGetTimerValue() );
+
+        while( !window->closeRequested() ) {
+            //
+            // Setup setup
+            //
+            time.tick( glfwGetTimerValue() );
+            inputs->frame( time.delta() );
+
+            glfwModule->context.pollEvents();
+            debug->frameStart();
+
+            //
+            // update step
+            //
+            scenes->update();
+
+            // render the scene
+            window->activate();
+            glModule->ogl.clear();
+
+            // allow the scene to do stuff, then actually render
+            scenes->render();
+
+            debug->draw();
+            window->swap();
+        }
+    */
 
-	return 0;
+	return app.run(argc, argv);
 }
diff --git a/fggl/CMakeLists.txt b/fggl/CMakeLists.txt
index a6974e6..2fde6fd 100644
--- a/fggl/CMakeLists.txt
+++ b/fggl/CMakeLists.txt
@@ -9,6 +9,7 @@ endif()
 target_sources(${PROJECT_NAME}
   PRIVATE
     fggl.cpp
+    app.cpp
     ecs/ecs.cpp
     data/model.cpp
     data/procedural.cpp
diff --git a/fggl/app.cpp b/fggl/app.cpp
new file mode 100644
index 0000000..a1b7b06
--- /dev/null
+++ b/fggl/app.cpp
@@ -0,0 +1,39 @@
+
+#include <cstdlib>
+#include <fggl/app.hpp>
+#include <fggl/util/states.hpp>
+
+namespace fggl {
+
+    App::App(const Identifer& name) : App::App( name, name ) {
+    }
+
+    App::App(const Identifer& name, const Identifer& folder ) : m_running(true), m_states() {
+    }
+
+    int App::run(int argc, const char** argv) {
+
+        {
+            // activate the first state
+            auto& state = m_states.active();
+            state.activate();
+        }
+
+        while ( m_running ) {
+            auto& state = m_states.active();
+
+            state.update();
+            state.render();
+        }
+
+        {
+            // shutdown last state
+            auto& state = m_states.active();
+            state.deactivate();
+        }
+
+        return EXIT_SUCCESS;
+    }
+
+} //namespace fggl
+
diff --git a/include/fggl/app.hpp b/include/fggl/app.hpp
new file mode 100644
index 0000000..04f2cc3
--- /dev/null
+++ b/include/fggl/app.hpp
@@ -0,0 +1,126 @@
+/**
+ * FOSS Galaxy Game Library
+ * Copyright (c) 2022 Joseph Walton-Rivers
+ * webpigeon@fossgalaxy.com
+ * 
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+#ifndef FGGL_APP_H
+#define FGGL_APP_H
+
+#define assertm(exp, msg) assert(((void)msg, exp))
+
+#include <cassert>
+#include <string>
+#include <memory>
+#include <unordered_map>
+#include <fggl/util/states.hpp>
+
+namespace fggl {
+
+    class App;
+    class AppState;
+
+    using Identifer = std::string;
+    using AppMachine = util::StateMachine<AppState, Identifer>;
+
+    class AppState {
+        public:
+            /**
+             * Create an Application State.
+             *
+             * A state is responsible for managing user interaction with the app. When created, the appstate
+             * is passed a reference to the application that owns it. The lifetime of the state is bounded
+             * by the lifetype of this object.
+             *
+             * @param owner a non-owned reference to the owner of the state.
+             */
+            explicit AppState(App& owner) : m_owner( owner ) {}
+
+            /**
+             * Update the underlying model of this state.
+             *
+             * States should not assume that one update means one render call, as the game loop may issue
+             * multiple updates per render or vice-versa depending on requriements. Update is intended for
+             * dispatching game-system related infomation.
+             */
+            virtual void update() = 0;
+
+            /**
+             * Perform actions neccerary for rendering the scene.
+             * 
+             * When this method is invoked it is safe to assume that rendering of some form will take place
+             * after it returns and the rendering state should be updated to reflect this. The rendering
+             * environment will be passed in as an argument.
+             *
+             * It is not safe to assume the render target will always be the same, as the scene may be
+             * rendered in mutliple passes (eg, for VR requirements).
+             */
+            virtual void render() = 0;
+
+            virtual void activate() {}
+            virtual void deactivate() {}
+
+        protected:
+            App& m_owner;
+    };
+
+    class App {
+        public:
+            explicit App(const Identifer& name);
+            App(const Identifer& name, const Identifer& folderName);
+
+            // class is non copy-able
+            App(const App& app) = delete;
+            App& operator=(App other) = delete;
+
+            /**
+             * Perform main game loop functions.
+             */
+            int run(int argc, const char** argv);
+
+            template<typename T>
+            void add_state(const Identifer& name) {
+                static_assert( std::is_base_of<AppState,T>::value, "States must be AppStates");
+                m_states.put<T>(name, *this);
+            }
+
+            inline void change_state(const Identifer& name) {
+                m_states.active().deactivate();
+                m_states.change(name);
+                m_states.active().activate();
+            }
+
+            inline AppState& active_state() const {
+                return m_states.active();
+            }
+
+            inline bool running() const {
+                return m_running;
+            }
+
+            inline void running(bool state) {
+                m_running = state;
+            }
+
+        private:
+            bool m_running;
+            AppMachine m_states;
+    };
+
+}
+
+#endif
diff --git a/include/fggl/fggl.hpp b/include/fggl/fggl.hpp
index e69de29..2363995 100644
--- a/include/fggl/fggl.hpp
+++ b/include/fggl/fggl.hpp
@@ -0,0 +1,30 @@
+/**
+ * FOSS Galaxy Game Library
+ * Copyright (c) 2022 Joseph Walton-Rivers
+ * webpigeon@fossgalaxy.com
+ * 
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+#ifndef FGGL_H
+#define FGGL_H
+
+#include <fggl/app.hpp>
+
+namespace fggl {
+
+}
+
+#endif
diff --git a/include/fggl/util/states.hpp b/include/fggl/util/states.hpp
new file mode 100644
index 0000000..66ca6d3
--- /dev/null
+++ b/include/fggl/util/states.hpp
@@ -0,0 +1,76 @@
+/**
+ * FOSS Galaxy Game Library
+ * Copyright (c) 2022 Joseph Walton-Rivers
+ * webpigeon@fossgalaxy.com
+ * 
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+#ifndef FGGL_UTIL_STATES_H
+#define FGGL_UTIL_STATES_H
+
+#include <cassert>
+#include <string>
+#include <memory>
+#include <unordered_map>
+
+namespace fggl::util {
+
+    template<typename S, typename I>
+    class StateMachine {
+        public:
+            using Identifer = I;
+            using StateType = S;
+
+            StateMachine() = default;
+            ~StateMachine() = default;
+
+            // class is non copy-able
+            StateMachine(const StateMachine& app) = delete;
+            StateMachine& operator=(StateMachine other) = delete;
+
+            template<typename T, typename... Args>
+            void put(const Identifer& name, Args&&... args) {
+                static_assert( std::is_base_of<StateType,T>::value, "States must be AppStates");
+                m_states[name] = std::make_unique<T>( std::forward<Args...>(args...) );
+
+                // no active scene? first is the active one
+                if ( m_active.empty() ) {
+                    m_active = name;
+                }
+            }
+
+            void change(const Identifer& name) {
+                assertm( m_states.find(name) != m_states.end(), "state does not exist");
+
+                active().deactivate();
+                m_active = name;
+                active().activate();
+            }
+
+            StateType& active() const {
+                assertm( m_states.find(m_active) != m_states.end(), "active state does not exist!");
+                return *(m_states.at( m_active ).get());
+            }
+
+        private:
+            Identifer m_active;
+            std::unordered_map<Identifer, std::unique_ptr<StateType>> m_states;
+
+    };
+
+}
+
+#endif
-- 
GitLab