diff --git a/CMakeLists.txt b/CMakeLists.txt index 28787168..d238068a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -19,6 +19,12 @@ set(CMAKE_EXPORT_COMPILE_COMMANDS True) # Tell CMake to ignore warnings from stuff in our dependencies. Surely GLM # knows what it is doing... https://7tv.app/emotes/61e8fae862858c6406126ced +IF (NOT WIN32) + set_property( + DIRECTORY build + PROPERTY COMPILE_OPTIONS "-w" + ) +ENDIF() # Add google test to CMake add_subdirectory(dependencies/google-test) @@ -57,6 +63,8 @@ add_custom_target(lint --inline-suppr # return error exit code 1 on failures --error-exitcode=1 + # ignore certain warnings that are annoying + --suppress=unusedVariable # tell it about our include directory -I ${CMAKE_SOURCE_DIR}/include # only check include and src @@ -66,4 +74,9 @@ add_custom_target(lint -i${CMAKE_SOURCE_DIR}/src/client/tests -i${CMAKE_SOURCE_DIR}/src/server/tests -i${CMAKE_SOURCE_DIR}/src/shared/tests -) \ No newline at end of file +) + +add_custom_target(pull_models + COMMAND + gdown 1N7a5cDgMcXbPO0RtgznnEo-1XUfdMScM -O ${CMAKE_SOURCE_DIR}/assets/graphics --folder +) diff --git a/README.md b/README.md index 77ac15af..ebda9bb7 100644 --- a/README.md +++ b/README.md @@ -132,6 +132,10 @@ Depending on where you need to link the library (client, server, shared), you wi - [C++ Intellisense](https://marketplace.visualstudio.com/items?itemName=ms-vscode.cpptools) - [General Productivity](https://marketplace.visualstudio.com/items?itemName=jirkavrba.subway-surfers) +## Models + +You can download models from our Google Drive folder [here](https://drive.google.com/drive/folders/1N7a5cDgMcXbPO0RtgznnEo-1XUfdMScM?usp=sharing) and place them in `src/client/models`. Alternatively, you can install [gdown](https://github.com/wkentaro/gdown) and run `make pull_models` to automatically pull them. + ## Documentation View deployed documentation [here](https://cse125.ucsd.edu/2024/cse125g3/site/docs/html/) diff --git a/assets/.DS_Store b/assets/.DS_Store new file mode 100644 index 00000000..df5a4be4 Binary files /dev/null and b/assets/.DS_Store differ diff --git a/assets/fonts/AncientModernTales-a7Po.ttf b/assets/fonts/AncientModernTales-a7Po.ttf new file mode 100644 index 00000000..c172fc80 Binary files /dev/null and b/assets/fonts/AncientModernTales-a7Po.ttf differ diff --git a/assets/graphics/.gitignore b/assets/graphics/.gitignore new file mode 100644 index 00000000..d6b7ef32 --- /dev/null +++ b/assets/graphics/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/assets/imgs/Yoshi.png b/assets/imgs/Yoshi.png new file mode 100644 index 00000000..2d9fd165 Binary files /dev/null and b/assets/imgs/Yoshi.png differ diff --git a/assets/imgs/awesomeface.png b/assets/imgs/awesomeface.png new file mode 100644 index 00000000..23c778d7 Binary files /dev/null and b/assets/imgs/awesomeface.png differ diff --git a/assets/imgs/pikachu.png b/assets/imgs/pikachu.png new file mode 100644 index 00000000..b0fa72a2 Binary files /dev/null and b/assets/imgs/pikachu.png differ diff --git a/config.json b/config.json index b6fa0fb4..9d1dc885 100644 --- a/config.json +++ b/config.json @@ -11,12 +11,13 @@ "server_port": 2355 }, "server": { - "lobby_name": "My Test Lobby", - "lobby_broadcast": false, + "lobby_name": "Unnamed Lobby", + "lobby_broadcast": true, "max_players": 1 }, "client": { - "default_name": "Player", - "lobby_discovery": false + "default_name": "John Doe", + "lobby_discovery": true, + "window_width": 1500 } } \ No newline at end of file diff --git a/dependencies/assimp/CMakeLists.txt b/dependencies/assimp/CMakeLists.txt new file mode 100644 index 00000000..13eb3a9c --- /dev/null +++ b/dependencies/assimp/CMakeLists.txt @@ -0,0 +1,15 @@ +include(FetchContent) + +FetchContent_Declare(assimp + GIT_REPOSITORY https://github.com/assimp/assimp + GIT_TAG v5.3.1 + GIT_PROGRESS TRUE +) + +FetchContent_MakeAvailable(assimp) + +set(ASSIMP_INCLUDE_DIRS + ${CMAKE_BINARY_DIR}/_deps/assimp-src/include/ + ${CMAKE_BINARY_DIR}/_deps/assimp-build/include/ + PARENT_SCOPE +) diff --git a/dependencies/freetype/CMakeLists.txt b/dependencies/freetype/CMakeLists.txt new file mode 100644 index 00000000..b62c76f2 --- /dev/null +++ b/dependencies/freetype/CMakeLists.txt @@ -0,0 +1,9 @@ +#shoutout chatgpt + +FetchContent_Declare( + freetype + GIT_REPOSITORY https://gitlab.freedesktop.org/freetype/freetype.git + GIT_TAG VER-2-13-2 +) + +FetchContent_MakeAvailable(freetype) \ No newline at end of file diff --git a/dependencies/glew/CMakeLists.txt b/dependencies/glew/CMakeLists.txt index 8a750c9b..3cf51284 100644 --- a/dependencies/glew/CMakeLists.txt +++ b/dependencies/glew/CMakeLists.txt @@ -8,6 +8,6 @@ FetchContent_Declare(glew FetchContent_MakeAvailable(glew) -# find_package(GLEW 2.0 REQUIRED) -# target_link_libraries(Starting GLEW::GLEW) - +set(GLEW_INCLUDE_DIRS + ${CMAKE_BINARY_DIR}/_deps/glew-src/include + PARENT_SCOPE) diff --git a/dependencies/imgui/CMakeLists.txt b/dependencies/imgui/CMakeLists.txt index 20be0641..29e78a2b 100644 --- a/dependencies/imgui/CMakeLists.txt +++ b/dependencies/imgui/CMakeLists.txt @@ -10,19 +10,20 @@ FetchContent_MakeAvailable(imgui) set(imgui-directory ${CMAKE_BINARY_DIR}/_deps/imgui-src/ PARENT_SCOPE) set(imgui-directory ${CMAKE_BINARY_DIR}/_deps/imgui-src/) -set(imgui-source -${imgui-directory}/imconfig.h - ${imgui-directory}/imgui.h +set(IMGUI_INCLUDE_DIRS + ${imgui-directory} + ${imgui-directory}/backends + PARENT_SCOPE +) + +set(IMGUI_SOURCES ${imgui-directory}/imgui.cpp ${imgui-directory}/imgui_draw.cpp - ${imgui-directory}/imgui_internal.h ${imgui-directory}/imgui_widgets.cpp - ${imgui-directory}/imstb_rectpack.h - ${imgui-directory}/imstb_textedit.h - ${imgui-directory}/imstb_truetype.h ${imgui-directory}/imgui_tables.cpp ${imgui-directory}/imgui_demo.cpp ${imgui-directory}/backends/imgui_impl_glfw.cpp ${imgui-directory}/backends/imgui_impl_opengl3.cpp + ${imgui-directory}misc/cpp/imgui_stdlib.cpp PARENT_SCOPE ) \ No newline at end of file diff --git a/dependencies/stb/CMakeLists.txt b/dependencies/stb/CMakeLists.txt new file mode 100644 index 00000000..84bd7aba --- /dev/null +++ b/dependencies/stb/CMakeLists.txt @@ -0,0 +1,11 @@ +include(FetchContent) + +FetchContent_Declare(stb + GIT_REPOSITORY https://github.com/nothings/stb + GIT_PROGRESS TRUE +) +FetchContent_MakeAvailable(stb) + +set(STB_INCLUDE_DIRS + ${CMAKE_BINARY_DIR}/_deps/stb-src/ + PARENT_SCOPE) diff --git a/include/client/camera.hpp b/include/client/camera.hpp index 9902b931..770814a7 100644 --- a/include/client/camera.hpp +++ b/include/client/camera.hpp @@ -30,6 +30,8 @@ class Camera { glm::mat4 getViewProj(); void updatePos(glm::vec3 pos); + glm::vec3 getPos(); + private: // Perspective controls float FOV; // Field of View Angle (degrees) diff --git a/include/client/client.hpp b/include/client/client.hpp index 3e2ecd54..a9be3df4 100644 --- a/include/client/client.hpp +++ b/include/client/client.hpp @@ -1,19 +1,22 @@ -#pragma once - #include "client/core.hpp" #include #include #include #include +#include #include #include #include #include "client/cube.hpp" +#include "client/lightsource.hpp" +#include "client/shader.hpp" +#include "client/model.hpp" #include "client/util.hpp" #include "client/lobbyfinder.hpp" +#include "client/gui/gui.hpp" #include "client/camera.hpp" //#include "shared/game/gamestate.hpp" @@ -22,6 +25,15 @@ #include "shared/network/session.hpp" #include "shared/utilities/config.hpp" +#define WINDOW_WIDTH Client::getWindowSize().x +#define WINDOW_HEIGHT Client::getWindowSize().y + +// position something a "frac" of the way across the screen +// e.g. WIDTH_FRAC(1, 4) -> a fourth of the way from the left +// HEIGHT_FRAC(2, 3) -> two thirds of the way from the bottom +#define FRAC_WINDOW_WIDTH(num, denom) Client::window_width * static_cast(num) / static_cast(denom) +#define FRAC_WINDOW_HEIGHT(num, denom) Client::window_height * static_cast(num) / static_cast(denom) + using namespace boost::asio::ip; class Client { @@ -29,13 +41,19 @@ class Client { public: // Callbacks void displayCallback(); + void idleCallback(boost::asio::io_context& context); void handleKeys(int eid, int keyType, bool keyHeld, bool *eventSent, glm::vec3 movement = glm::vec3(0.0f)); // Bound window callbacks static void keyCallback(GLFWwindow *window, int key, int scancode, int action, int mods); - static void mouseCallback(GLFWwindow *window, double xposIn, double yposIn); + static void mouseCallback(GLFWwindow* window, double xposIn, double yposIn); + static void mouseButtonCallback(GLFWwindow* window, int button, int action, int mods); + static void charCallback(GLFWwindow* window, unsigned int codepoint); + + static glm::vec2 getWindowSize(); + static time_t getTimeOfLastKeystroke(); // Getter / Setters GLFWwindow* getWindow() { return window; } @@ -49,19 +67,27 @@ class Client { void draw(); void connectAndListen(std::string ip_addr); - boost::filesystem::path getRootPath(); - private: void processClientInput(); void processServerInput(boost::asio::io_context& context); SharedGameState gameState; - float cubeMovementDelta = 0.05f; + std::shared_ptr cube_shader; + std::shared_ptr model_shader; + std::shared_ptr light_source_shader; + + std::unique_ptr player_model; + std::unique_ptr bear_model; + std::unique_ptr light_source; + + float playerMovementDelta = 0.05f; GLFWwindow *window; - GLuint cubeShaderProgram; + friend class gui::GUI; + gui::GUI gui; + gui::GUIState gui_state; Camera *cam; // Flags @@ -77,17 +103,24 @@ class Client { static bool cam_is_held_right; static bool cam_is_held_left; + static bool is_left_mouse_down; + static bool is_click_available; static float mouse_xpos; static float mouse_ypos; + static int window_width; + static int window_height; + + static time_t time_of_last_keystroke; + GameConfig config; tcp::resolver resolver; tcp::socket socket; + LobbyFinder lobby_finder; + /// @brief Generate endpoints the client can connect to basic_resolver_results endpoints; std::shared_ptr session; - - boost::filesystem::path root_path; }; diff --git a/include/client/constants.hpp b/include/client/constants.hpp new file mode 100644 index 00000000..57432c67 --- /dev/null +++ b/include/client/constants.hpp @@ -0,0 +1,11 @@ +#pragma once + +#define MIN_WINDOW_WIDTH 900 +#define MIN_WINDOW_HEIGHT 600 + +// The screen width that's defined as 1:1, from which other +// resolutions can calculate pixel sizes +#define UNIT_WINDOW_WIDTH 1500 +#define UNIT_WINDOW_HEIGHT 1000 + +#define PLAYER_EYE_LEVEL 2.0f \ No newline at end of file diff --git a/include/client/core.hpp b/include/client/core.hpp index 5d1d1ba0..6ce73f45 100644 --- a/include/client/core.hpp +++ b/include/client/core.hpp @@ -1,3 +1,4 @@ #include #include -#include \ No newline at end of file +#include +#include \ No newline at end of file diff --git a/include/client/cube.hpp b/include/client/cube.hpp index 20ea8d55..58cf1574 100644 --- a/include/client/cube.hpp +++ b/include/client/cube.hpp @@ -1,6 +1,7 @@ #pragma once #include "client/core.hpp" +#include "client/renderable.hpp" #define GLM_ENABLE_EXPERIMENTAL #include @@ -11,23 +12,22 @@ #include #include -class Cube { +class Cube : public Renderable { public: - Cube(glm::vec3 newColor, glm::vec3 scale); + explicit Cube(glm::vec3 newColor); ~Cube(); - void draw(glm::mat4 viewProjMat, GLuint shader, bool fill); - void update(glm::vec3 new_pos); - void update_delta(glm::vec3 delta); - + void draw(std::shared_ptr shader, + glm::mat4 viewProj, + glm::vec3 camPos, + glm::vec3 lightPos, + bool fill) override; private: GLuint VAO; GLuint VBO_positions, VBO_normals, EBO; - glm::mat4 model; glm::vec3 color; - // Cube Information std::vector positions; std::vector normals; diff --git a/include/client/gui/font/font.hpp b/include/client/gui/font/font.hpp new file mode 100644 index 00000000..03aebbc4 --- /dev/null +++ b/include/client/gui/font/font.hpp @@ -0,0 +1,63 @@ +#pragma once + +#include "client/core.hpp" + +#include +#include +#include + +namespace gui::font { + + +/** + * Abstract representation of the different fonts to use in our game + * + * NOTE: currently I haven't found a good font for "Text", so both of these + * map to the same font. + */ +enum class Font { + MENU, + TEXT +}; + +/** + * Mappings from our specified abstract fonts to the file to load + */ +std::string getFilepath(Font font); + +/** + * Preset colors for text + */ +enum class Color { + BLACK, + RED, + BLUE, + GRAY, + WHITE, + TORCHLIGHT_GAMES +}; + +/** + * Mapping from preset font colors to RGB values + */ +glm::vec3 getRGB(Color color); + +const int UNIT_LARGE_SIZE_PX = 128; // how many pixels a small font is on the unit screen size +enum class Size { + SMALL, + MEDIUM, + LARGE, + XLARGE +}; +const std::unordered_map SIZE_TO_SCALE = { + {Size::SMALL, 0.25f}, + {Size::MEDIUM, 0.50f}, + {Size::LARGE, 1.0f}, + {Size::XLARGE, 2.0f}, +}; + +int getFontSizePx(Size size); +float getScaleFactor(Size size); + + +} diff --git a/include/client/gui/font/loader.hpp b/include/client/gui/font/loader.hpp new file mode 100644 index 00000000..b991d823 --- /dev/null +++ b/include/client/gui/font/loader.hpp @@ -0,0 +1,63 @@ +#pragma once + +#include "client/core.hpp" +#include "client/gui/font/font.hpp" + +// freetype needs this extra include for whatever unholy reason +#include +#include FT_FREETYPE_H + +#include + +namespace gui::font { + +// modified from https://learnopengl.com/In-Practice/Text-Rendering + +/** + * Representation of a font character + */ +struct Character { + unsigned int texture_id; /// id handle for glyph texture + glm::ivec2 size; /// size of glyph + glm::ivec2 bearing; /// offset from baseline to left/top of glyph + unsigned int advance; /// offset to advance to next glyph +}; + +/** + * Handles loading all of the fonts we want to use, and provides an interface to get + * the Character information (e.g. opengl texture and sizing information). + */ +class Loader { +public: + Loader() = default; + + /** + * Initializes all of the font information for every font we want to use and stores + * it inside of the font map data member. + */ + bool init(); + + /** + * Loads the specified character with the specified font and size + * + * @param c ASCII char to load + * @param font Abstract font to use. + * @returns the Character information for that glyph + */ + [[nodiscard]] const Character& loadChar(char c, Font font) const; + +private: + FT_Library ft; + + /** + * Internal helper function to load a font. Called in `init()` + */ + bool _loadFont(Font font); + + std::unordered_map< + Font, + std::unordered_map + > font_map; +}; + +} diff --git a/include/client/gui/gui.hpp b/include/client/gui/gui.hpp new file mode 100644 index 00000000..07293259 --- /dev/null +++ b/include/client/gui/gui.hpp @@ -0,0 +1,359 @@ +#pragma once + +// Include all gui headers so everyone else just needs to include this file +#include "client/gui/widget/options.hpp" +#include "client/gui/widget/type.hpp" +#include "client/gui/widget/widget.hpp" +#include "client/gui/widget/dyntext.hpp" +#include "client/gui/widget/flexbox.hpp" +#include "client/gui/widget/staticimg.hpp" +#include "client/gui/widget/centertext.hpp" +#include "client/gui/widget/textinput.hpp" +#include "client/gui/font/font.hpp" +#include "client/gui/font/loader.hpp" +#include "client/gui/img/img.hpp" +#include "client/gui/img/loader.hpp" + +#include +#include +#include + +class Client; + +namespace gui { + +/** + * Enumeration for all of the different "screens" that can be rendered by the GUI. The GUI class + * itself doesn't contain any internal state related to any specific state, but instead takes + * a GUIState as input to determine what should be rendered to the screen. + */ +enum class GUIState { + NONE, + INITIAL_LOAD, + TITLE_SCREEN, + LOBBY_BROWSER, + LOBBY, + GAME_HUD, + GAME_ESC_MENU +}; + +#define GUI_PROJECTION_MATRIX() glm::ortho(0.0f, (float)WINDOW_WIDTH, 0.0f, (float)WINDOW_HEIGHT); + +/** + * Class which wraps around all of the GUI elements that should be rendered to the screen. + * + * The GUI can essentially be thought of as a collection of "Widgets" which all have "Handles" (or IDs). + * Each widget has all of the logic it needs to know its size, location, and how to render it. + * The GUI class acts as a container for all of these widgets and provides a helpful abstraction layer + * allowing the rest of the code to think in terms of "GUIState", since you can just tell the GUI + * to render a specific state, and it will do so. + */ +class GUI { +public: + /// ========================================================================= + /// + /// These are the functions that need to be called to setup a GUI object. Doing anything + /// else before calling these will probably cause a crash. + /// + /** + * @brief Stores the client pointer as a data member. + * + * Constructor for a GUI. This really does nothing except store a pointer to the Client object + * so that the GUI functions can easily access Client data members, due to the friend relationship + * of Client -> GUI. + * + * @param client Pointer to the client object. Note that GUI is a friend class to client, so GUI can + * access private client data members for ease of use. + */ + explicit GUI(Client* client); + /** + * @brief Initializes all of the necessary file loading for all of the GUI elements, and + * registers all of the static shader variables for each of the derived widget classes + * that need a shader. + */ + bool init(); + /// ================================================================================ + + /// ===================================================================== + /// + /// These series of functions are what you actually use to do the displaying and + /// rendering of the GUI. They should be called in the order they are listed in + /// this file. + /// + /** + * @brief Wipes all of the previous frame's state + * + * Function that should be called at the beginning of a frame. Essentially it wipes + * all of the previous frame's widget data. + */ + void beginFrame(); + /** + * @brief Adds widgets to the layout depending on the specified GUI state. + * + * @param state Current State of the GUI that should be rendered. This essentially + * corresponds to a specific "screen" that should be displayed + */ + void layoutFrame(GUIState state); + /** + * @brief Takes the current relevant input information and alters the GUI based on the + * how the inputs interact with all of the handlers assigned to the Widgets. + * + * NOTE: this must be called after adding all of the widgets to the screen, otherwise + * no event handling will work on those widgets added after calling this function. + * + * NOTE: currently this function takes a reference to the mouse down boolean because + * if it "captures" a mouse click then it sets that boolean to false, to prevent continued + * event triggers if the mouse is held down. In other words, we want "onClick" events + * to happen only on the initial click, not continually while the mouse is held down. + * + * NOTE: both of the x and y coordinates of the mouse passed into this function are in + * the GLFW coordinate space. In GLFW coordinates, the top left corner of the screen is + * (x=0,y=0). However, in the GUI world all of our coordinates are based off of + * (x=0,y=0) being in the bottom left corner. This is the only GUI function that takes + * GLFW coordinates, + * + * @param mouse_xpos x position of the mouse, in GLFW Coordinates + * @param mouse_ypos y position of the mouse, in GLFW Coordinates + * @param is_left_mouse_down reference to flag which stores whether or not the left mouse + * is down. Note: this is a reference so that if a click is captured it can toggle the + * mouse down flag to false, so that click doesn't get "double counted" in subsequent + * frames. + */ + void handleInputs(float mouse_xpos, float mouse_ypos, bool& is_left_mouse_down); + /** + * Renders the current state of the GUI to the screen, based on all of the widgets + * that have been added and any changes caused by inputs. + */ + void renderFrame(); + /// ============================================================================== + + /// ========================================================= + /// + /// These functions are concerned with adding, removing, and getting widgets to/from + /// the GUI. + /// + /** + * @brief Adds the specified widget to the GUI. + * + * NOTE: the widget that is being passed in is of the type Widget::Ptr, which is + * a typedef for a std::unique_ptr. To easily get a unique_ptr for a Widget, + * you can use the corresponding static `make` function provided by each widget + * implementation. + * + * @param widget Widget to add + * @returns A handle to the widget that was added, so it can be modifed/removed later + */ + widget::Handle addWidget(widget::Widget::Ptr&& widget); + /** + * @brief Removes the specified widget from the GUI + * + * @param handle Handle to the widget that you want to remove + * @returns The widget that you removed, now isolated from the GUI. + */ + widget::Widget::Ptr removeWidget(widget::Handle handle); + /** + * @brief Borrows the specified widget from the GUI. + * This is especially useful inside of callback functions for widgets as you will + * already have the handle needed to initiate a borrow. + * + * NOTE: This function essentially returns the raw pointer for the specified widget. + * Technically you could do unspeakable things to this pointer, but as we are all + * bound by societal conventions so shall you too not break this taboo. + * + * NOTE: You should not attempt to save this pointer elsewhere. This pointer can be thought + * of as a "temporary" reference to the Widget, which will go out of scope + * between the time of calling and the next frame. + * + * NOTE: The template argument you specify essentially performs a dynamic cast on the + * returned pointer. You should not make your template argument a pointer itself, since + * the function return type already adds a pointer to it. You should only specify a + * specific kind of Widget if you are confident in what kind of widget it is (i.e. in + * an event handler). Otherwise, it is safe to just specify a widget::Widget as the + * template argument + * + * Example use: + * // Precondition: We know that `handle` is a valid handle to a widget::DynText + * auto widget = gui.borrowWidget(handle); + * // widget is of type `widget::DynText*` + * + * If you mess up and pass in the wrong template argument, then the pointer will + * end up being nullptr. + * + * @tparam Type of the widget that you are trying to borrow. + * @param handle Handle to the widget you want to borrow + * @returns Pointer to the widget specified by the handle, casted to + * be a pointer of the specified template type. + */ + template + W* borrowWidget(widget::Handle handle) { + for (const auto& [_, widget] : this->widgets) { + if (widget->hasHandle(handle)) { + return dynamic_cast(widget->borrow(handle)); + } + } + + std::cerr << "GUI ERROR: attempting to borrowWidget from GUI\n" + << "with invalid handle. This should never happen\n" + << "and means we are doing something very very bad." << std::endl; + std::exit(1); + } + /// ============================================================================== + + /// ============================================================== + /// + /// These functions are concerned with keyboard input for the TextInput widget. + /// Because of the way this is implemented, there can only be one TextInput widget + /// on the screen at one time. If there are more, they will end up sharing all of + /// the inputted text. + /// + /** + * @brief Checks to see if the GUI is currently capturing keyboard input. + * + * NOTE: This shouldn't be called as a precondition for `captureKeystroke` because + * `captureKeystroke` will internally also check whether or not the GUI is + * capturing keystrokes. + * + * @returns true if the GUI is capturing keyboard input, false otherwise + */ + bool shouldCaptureKeystrokes() const; + /** + * @brief Toggles whether or not the GUI should be capturing keyboard input + * + * @param should_capture Whether or not the GUI should be capturing keyboard input + */ + void setCaptureKeystrokes(bool should_capture); + /** + * @brief Captures a keystroke as an ASCII char. + * + * Takes a keystroke as captured by the client's GLFW code, and adds it into the GUI. + * Internally, this will check that the ASCII value is between [32, 126], which is the + * set of meaningful ASCII values that we care about. + * + * NOTE: this function internally checks whether or not the GUI is capturing keyboard + * input. If the GUI is not currently capturing keyboard input, then this will do + * nothing. This means that you don't need to check `shouldCaptureKeystrokes` before + * calling this function. + * + * NOTE: This function does not handle backspaces. To handle backspaces, the + * `captureBackspace` function should be used instead. + * + * @param c ASCII char to capture + */ + void captureKeystroke(char c); + /** + * @brief If the GUI is capturing backspaces, then records a backspace press. This deletes + * the most recently captured keystroke in the GUI. + */ + void captureBackspace(); + /** + * @brief Wipes all of the captured keyboard input from the internal state. + */ + void clearCapturedKeyboardInput(); + /** + * @brief Returns all of the captured keyboard input without clearing it. + * + * @returns all of the captured keyboard input as an std::string + */ + std::string getCapturedKeyboardInput() const; + /// ============================================================================== + + /// ===================================================================== + /// + /// Getters for various private data members + /// + /** + * @brief Getter for the font loader + * @returns shared pointer to the font loader + */ + std::shared_ptr getFonts(); + /// ============================================================================== + +private: + widget::Handle next_handle {0}; + std::unordered_map widgets; + + std::shared_ptr fonts; + img::Loader images; + + bool capture_keystrokes; + std::string keyboard_input; + + Client* client; + + /// =========================================================== + /** + * @brief Performs a click on the specied coordinate in the GUI coordinate frame. + * + * This is what will call the click callback functions on the widgets. + * + * @param x x coordinate of click in GUI coordinates + * @param y y coordinate of click in GUI coordinates + */ + void _handleClick(float x, float y); + /** + * @brief Performs a mouse hover on the specied coordinate in the GUI coordinate frame. + * + * This is what will call the hover callback functions on the widgets. + * + * @param x x coordinate of click in GUI coordinates + * @param y y coordinate of click in GUI coordinates + */ + void _handleHover(float x, float y); + /// ============================================================================= + + /// ================================================================ + /// + /// These are all of the internal helper functions which render a specified GUIState + /// layout. These are where all of the widget manipulation should occur. + /// + /// NOTE: The widget manipulation functions are public, so you can also do further + /// widget manipulation outside of one of these functions. However, if you feel the + /// need to do this you should consider whether or not what you're doing makes more + /// sense to encode as a GUIState. The reason those functions are public are to do + /// more fine tuned GUI manipulation which may only make sense to do outside of these + /// preset "layouts". + /// + /** + * @brief Displays a loading screen while the game is starting up + */ + void _layoutLoadingScreen(); + /** + * @brief Displays the title screen layout + * + * Transitions to the LobbyBrowser once "Start Game" is clicked. + */ + void _layoutTitleScreen(); + /** + * @brief Displays the lobby browser layout + * + * Allows the user to input a name. + * Transitions to the Lobby once a lobby is selected to join. + */ + void _layoutLobbyBrowser(); + /** + * @brief Displays the lobby layout + * + * Does not provide any GUI-initiated transitions, as it instead waits for the + * server to specify that the game has started with a LoadGameStateEvent + * + * Displays the lobby name and all of the players who are currently in the lobby. + */ + void _layoutLobby(); + /** + * @brief Displays the Game HUD layout + * + * TODO: this is not implemented yet + */ + void _layoutGameHUD(); + /** + * @brief Displays the menu which appears when the player presses Escape while playing + * + * Known bugs: + * BUG: The game stops rendering the game when this is being displayed + * BUG: Mouse movement is still tracked causing disorienting reorientation upon resuming + */ + void _layoutGameEscMenu(); + /// ============================================================================= +}; + +} diff --git a/include/client/gui/img/img.hpp b/include/client/gui/img/img.hpp new file mode 100644 index 00000000..d5b47f25 --- /dev/null +++ b/include/client/gui/img/img.hpp @@ -0,0 +1,40 @@ +#pragma once + +#include "shared/utilities/root_path.hpp" +#include "client/core.hpp" + +#include +#include + +namespace gui::img { + +/** + * Abstract representation of any image we would want to load in + * + * NOTE: if you add a new ID, then also add it into the below macro so that + * the image loader will actually load it. + */ +enum class ImgID { + Yoshi, + Pikachu +}; +#define GET_ALL_IMG_IDS() \ + {ImgID::Yoshi, ImgID::Pikachu} + +/** + * Representation of a loaded image + */ +struct Img { + GLuint texture_id; /// opengl texture id + int width; /// width in pixels + int height; /// height in pixels +}; + +/** + * Mapping from abstract image id to the filepath for that image + * + * @param img ID of the image to get the filepath of + */ +std::string getImgFilepath(ImgID img); + +} \ No newline at end of file diff --git a/include/client/gui/img/loader.hpp b/include/client/gui/img/loader.hpp new file mode 100644 index 00000000..fe85d567 --- /dev/null +++ b/include/client/gui/img/loader.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include "client/gui/img/img.hpp" + +#include + +namespace gui::img { + +/** + * This class loads in all of our images + */ +class Loader { +public: + Loader() = default; + + bool init(); + + const Img& getImg(ImgID img_id) const; + +private: + std::unordered_map img_map; + + bool _loadImg(ImgID id); +}; + +} diff --git a/include/client/gui/widget/centertext.hpp b/include/client/gui/widget/centertext.hpp new file mode 100644 index 00000000..45a25a04 --- /dev/null +++ b/include/client/gui/widget/centertext.hpp @@ -0,0 +1,37 @@ +#pragma once + +#include "client/gui/widget/flexbox.hpp" +#include "client/gui/widget/dyntext.hpp" + +namespace gui::widget { + +/** + * This widget is a special kind of "Macro Widget" which essentially acts as a wrapper around a more + * complex internal structure to make a piece of text that is centered horizontally across the + * screen. + */ +class CenterText { +public: + /** + * @brief Constructs a unique_ptr for a Flexbox widget that contains a DynText widget + * + * @param text Text to display + * @param font font to render the text with + * @param size size of the font + * @param color color of the text + * @param fonts font loader + * @param y_pos bottom y position of the widget in GUI coordinates + * + * @returns a Flexbox widget that centers an internal DynText widget + */ + static Widget::Ptr make( + std::string text, + font::Font font, + font::Size size, + font::Color color, + std::shared_ptr fonts, + float y_pos + ); +}; + +} diff --git a/include/client/gui/widget/dyntext.hpp b/include/client/gui/widget/dyntext.hpp new file mode 100644 index 00000000..e6f189a8 --- /dev/null +++ b/include/client/gui/widget/dyntext.hpp @@ -0,0 +1,59 @@ +#pragma once + +#include +#include + +#include "client/core.hpp" +#include "client/shader.hpp" +#include "client/gui/widget/widget.hpp" +#include "client/gui/font/font.hpp" +#include "client/gui/font/loader.hpp" + +namespace gui::widget { + +class DynText : public Widget { +public: + using Ptr = std::unique_ptr; + static std::unique_ptr shader; + + struct Options { + Options(font::Font font, font::Size size, font::Color color): + font(font), size(size), color(color) {} + + font::Font font {font::Font::TEXT}; + font::Size size {font::Size::SMALL}; + font::Color color {font::Color::BLACK}; + }; + + /** + * @brief creates a DynText unique ptr widget + * + * @param origin (Optional) Bottom left coordinate position of the widget in GUI coordinates + * @param text Text to render + * @param load Font loader + * @param options (Optional) Options to customize how the text is rendered + * + * @returns a DynText widget + */ + template + static Ptr make(Params&&... params) { + return std::make_unique(std::forward(params)...); + } + + DynText(glm::vec2 origin, const std::string& text, std::shared_ptr loader, Options options); + DynText(const std::string& text, std::shared_ptr loader, Options options); + + void render() override; + + void changeColor(font::Color new_color); + +private: + Options options; + std::string text; + std::shared_ptr fonts; + + unsigned int VAO; + unsigned int VBO; +}; + +} diff --git a/include/client/gui/widget/flexbox.hpp b/include/client/gui/widget/flexbox.hpp new file mode 100644 index 00000000..58eb5729 --- /dev/null +++ b/include/client/gui/widget/flexbox.hpp @@ -0,0 +1,64 @@ +#pragma once + +#include "client/gui/widget/widget.hpp" + +#include +#include + +namespace gui::widget { + +class Flexbox : public Widget { +public: + using Ptr = std::unique_ptr; + + struct Options { + Options(Justify direction, Align alignment, float padding): + direction(direction), alignment(alignment), padding(padding) {} + + Justify direction; + Align alignment; + float padding; + }; + + /** + * @brief creates a flexbox unique ptr for use in the GUI + * + * NOTE: I don't think flexboxes inside of flexboxes will currently work, but I haven't tried. + * + * @param origin Bottom left (x,y) coordinate of the flexbox, from which the container + * grows upward + * @param size (Optional) Size of the container in pixels. You should only specify a size + * in the axis that does not grow in the direction of pushing (e.g. set a static width if you + * have a VERTICAL flexbox). + * @param options (Optional) Configurable options for the flexbox. + * + * @returns A Flexbox widget + */ + template + static Ptr make(Params&&... params) { + return std::make_unique(std::forward(params)...); + } + + Flexbox(glm::vec2 origin, glm::vec2 size, Options options); + Flexbox(glm::vec2 origin, Options options); + + void doClick(float x, float y) override; + void doHover(float x, float y) override; + + /** + * @brief adds a new widget to the flexbox, from the bottom up + */ + void push(Widget::Ptr&& widget); + + void render() override; + + Widget* borrow(Handle handle) override; + bool hasHandle(Handle handle) const override; + +private: + Options options; + std::vector widgets; +}; + + +} diff --git a/include/client/gui/widget/options.hpp b/include/client/gui/widget/options.hpp new file mode 100644 index 00000000..eca38ff0 --- /dev/null +++ b/include/client/gui/widget/options.hpp @@ -0,0 +1,16 @@ +#pragma once + +namespace gui::widget { + +enum class Justify { + VERTICAL, + HORIZONTAL +}; + +enum class Align { + CENTER, + LEFT, + RIGHT +}; + +} \ No newline at end of file diff --git a/include/client/gui/widget/staticimg.hpp b/include/client/gui/widget/staticimg.hpp new file mode 100644 index 00000000..7e5fa6b6 --- /dev/null +++ b/include/client/gui/widget/staticimg.hpp @@ -0,0 +1,51 @@ +#pragma once + +#include "client/gui/widget/widget.hpp" +#include "client/gui/img/loader.hpp" +#include "client/gui/img/img.hpp" +#include "client/shader.hpp" + +#include + +namespace gui::widget { + + +/** + * Widget to display a static image (png) to the screen. + * + * BUG: This doesn't work at all currently! ASDAJSHHASHDAHHHHHHH! + * + * Reference: The chapter on sprite rendering from + * https://learnopengl.com/book/book_pdf.pdf + */ +class StaticImg : public Widget { +public: + using Ptr = std::unique_ptr; + static std::unique_ptr shader; + + /** + * @brief creates a StaticImg unique ptr widget + * + * @param origin (Optional) bottom left coordinate of the widget in GUI Coordinates + * @param img Id for the image to render + * + * @returns A StaticImg widget + */ + template + static Ptr make(Params&&... params) { + return std::make_unique(std::forward(params)...); + } + + StaticImg(glm::vec2 origin, gui::img::Img img); + explicit StaticImg(gui::img::Img img); + ~StaticImg(); + + void render() override; + +private: + gui::img::Img img; + GLuint quadVAO, VBO, EBO; + GLuint texture_id; +}; + +} diff --git a/include/client/gui/widget/textinput.hpp b/include/client/gui/widget/textinput.hpp new file mode 100644 index 00000000..54c0df6b --- /dev/null +++ b/include/client/gui/widget/textinput.hpp @@ -0,0 +1,62 @@ +#pragma once + +#include "client/gui/widget/widget.hpp" +#include "client/gui/widget/dyntext.hpp" +#include "client/gui/font/font.hpp" +#include "client/gui/font/loader.hpp" + +#include +#include + +namespace gui { + class GUI; +} + +namespace gui::widget { + +/** + * Widget to capture text input. + * NOTE: with the current implementation, there can only be one of these active in the GUI + * at one time. If there are multiple, they will both affect each other. + */ +class TextInput : public Widget { +public: + using Ptr = std::unique_ptr; + + /** + * @brief Constructs a TextInput unique ptr widget + * + * @param Origin bottom left (x,y) coordinate in GUI coordinates + * @param placeholder Text to display if no text has been inputted yet + * @param gui Pointer to the GUI class, so internally we can access keystroke information + * @param fonts Font loader + * @param options (Optional) Configurable options for the widget + */ + template + static Ptr make(Params&&... params) { + return std::make_unique(std::forward(params)...); + } + + TextInput(glm::vec2 origin, + const std::string& placeholder, + gui::GUI* gui, + std::shared_ptr fonts, + DynText::Options options); + + void render() override; + + bool hasHandle(Handle handle) const override; + Widget* borrow(Handle handle) override; + void doClick(float x, float y) override; + void doHover(float x, float y) override; + +private: + static std::string prev_input; + + widget::DynText::Ptr dyntext; + + /// NOTE: this widget needs a pointer to the GUI so it can access the GUI's keystroke capturing functionality + gui::GUI* gui; +}; + +} diff --git a/include/client/gui/widget/type.hpp b/include/client/gui/widget/type.hpp new file mode 100644 index 00000000..efb920d7 --- /dev/null +++ b/include/client/gui/widget/type.hpp @@ -0,0 +1,9 @@ +#pragma once + +namespace gui::widget { + +enum class Type { + DynText, Flexbox, StaticImg, TextInput +}; + +} diff --git a/include/client/gui/widget/widget.hpp b/include/client/gui/widget/widget.hpp new file mode 100644 index 00000000..fbcc8162 --- /dev/null +++ b/include/client/gui/widget/widget.hpp @@ -0,0 +1,236 @@ +#pragma once + +#include "client/core.hpp" +#include "client/gui/widget/type.hpp" +#include "client/gui/widget/options.hpp" + +#include +#include +#include +#include +#include + +namespace gui::widget { + +/// @brief Type for a Widget Handle +using Handle = std::size_t; +/// @brief Type for the handle for a specific callback function +using CallbackHandle = std::size_t; +/// @brief Type for a onClick or onHover callback function. +/// The Handle parameter is the handle to the widget that the callback is +/// attached to. +using Callback = std::function; + +/** + * Abstract base class for any arbitrary widget. + * + * Any Widget implementation must override the following pure virtual functions: + * - render + * + * And certain widget implementations may also wish to override these virtual functions + * - doClick + * - doHover + * - hasHandle + * - borrow + * + * You can see the documentation for these functions for explanations of why you may or + * may not need to override this functions for a specific derived widget. + * + * In addition, any derived class must also set width and height once these values are known. + */ +class Widget { +public: + /// All widgets are passed around and manipulated as unique ptrs so this is a helpful + /// alias to reduce characters typed + using Ptr = std::unique_ptr; + + /// =============================================================================== + /// + /// These functions are necessary to setup and position a widget on the screen. + /// + /** + * @brief Sets the type and origin position of the widget, and assigns a new unique handle. + * + * @param type Type of the widget + * @param origin Origin position of the widget (bottom left x,y coordinate in the GUI + * coordinate frame) + */ + explicit Widget(Type type, glm::vec2 origin); + /** + * @brief Overrides the current origin position with the newly specified one. + * + * NOTE: This is helpful when you may not know the exact origin position of a widget at + * construction time (i.e. when inserting a widget inside of a flexbox, as the flexbox) + * then becomes responsible for positioning the widget). + * + * @param origin Bottom left (x,y) coordinate of the widget in the GUI coordinate frame. + */ + void setOrigin(glm::vec2 origin); + /// ====================================================================================== + + /// ===================================================================== + /// + /// These functions are used to register, remove, and activate action callbacks on widgets + /// + /** + * @brief Register a new onClick callback function for this widget. + * + * @param callback Callback function to run on click + * + * @returns handle to the callback function, which can be passed into removeOnClick to + * unregister. + */ + CallbackHandle addOnClick(Callback callback); + /** + * @brief Register a new onHover callback function for this widget. + * + * @param callback Callback function to run on hover + * + * @returns handle to the callback function, which can be passed into removeOnHover to + * unregister. + */ + CallbackHandle addOnHover(Callback callback); + /** + * @brief Removes the onClick callback function associated with the given handle + * + * @param handle Handle to the onClick callback function you want to remove + */ + void removeOnClick(CallbackHandle handle); + /** + * @brief Removes the onHover callback function associated with the given handle + * + * @param handle Handle to the onHover callback function you want to remove + */ + void removeOnHover(CallbackHandle handle); + /** + * @brief Performs a click at the specified (x,y) position in the GUI coordinate frame. + * + * NOTE: This function's main role is to determine if a click at the specified (x,y) + * coordinate should trigger the onClick handlers for this widget. The default implementation + * of this checks to see if the (x,y) coordinate is within the bounds specified by `origin` + * and `width` and `height`, and if so calls all of the onClick handlers. + * + * NOTE: One reason for a derived class to override this function is if the widget itself + * contains other widgets, because in that case doClick will not be called on those "subwidgets" + * unless this function passes the call down the chain + * + * @param x x coordinate of the click in GUI coordinates + * @param y y coordinate of the click in GUI coordinates + */ + virtual void doClick(float x, float y); + /** + * @brief Performs a hover action at the specified (x,y) position in the GUI coordinate frame + * + * NOTE: see the documentation for `doClick`, as everything said there also applies here, but just + * for the hover actions. + */ + virtual void doHover(float x, float y); + /// ====================================================================================== + + /// ============================================================================= + /** + * @brief Renders the widget to the screen + * + * This is the only function that must be overridden by derived classes. + */ + virtual void render() = 0; + /// ====================================================================================== + + /// ====================================================================== + /** + * @brief Queries the widget::Type of the widget + * @return Type of the widget + */ + [[nodiscard]] Type getType() const; + /** + * @brief Queries the origin position of the widget + * @return origin position of the widget in GUI coordinates + */ + [[nodiscard]] const glm::vec2& getOrigin() const; + /** + * @brief Queries the size of the widget in pixels + * @return the width and height of the widget + */ + [[nodiscard]] std::pair getSize() const; + /** + * @brief Queries the handle for this specific widget + * @return the handle for this widget + */ + [[nodiscard]] Handle getHandle() const; + /// ====================================================================================== + + /// ==================================================================== + /// + /// The following functions are incredibly important for GUI manipulation, especially in + /// event handlers. They essentially define how outside users can gain internal access to + /// widgets that are inside of the GUI + /// + /// hasHandle should always be called before calling borrow if you aren't sure whether or + /// not the widget actually has the specified handle, because borrow will terminate the + /// program if the specified widget does not have the handle. + /// + /// By default hasHandle and borrow only check the widget itself to see if the handle matches. + /// This means that if some derived Widget acts as a container for some set of internal + /// subwidgets, then these functions should be overridden to provide access to those subwidgets. + /// + /** + * @brief Checks to see whether or not this widget "has" the specified handle, whether + * because the handle is this widgets handle, or because this widget internally contains + * some subwidget that has that handle. + * + * @param handle Handle of the widget for which you want to query + * + * @returns True if this widget is or contains the specified widget, false otherwise + */ + [[nodiscard]] virtual bool hasHandle(Handle handle) const; + /** + * @brief Gives a pointer to the subwidget specified by the handle + * + * NOTE: if `handle` is not a valid handle for either (1) this widget or (2) any subwidgets, + * then this function will terminate the program and print out an error message. + * + * @returns a pointer to the widget specified by handle + */ + [[nodiscard]] virtual Widget* borrow(Handle handle); + /// ====================================================================================== + +protected: + /// @brief Handle for this widget + Handle handle; + + /// @brief Static counter for the number of widgets that have been created, so a new identifier + /// can be assigned to each widget as they are created + static std::size_t num_widgets; + + /// @brief Type of the widget + Type type; + + /// @brief Origin position (bottom left) of the widget in GUI coordinates + glm::vec2 origin; + + /// NOTE: Both the widget and the height of the widget are initialized to 0 and are not set + /// anywhere inside of this base class. Instead, derived classes must set these values themselves + /// once they are known. + + /// @brief Width of the widget, in pixels + std::size_t width {0}; + /// @brief Height of the widget, in pixels + std::size_t height {0}; + + /// @brief all of the onClick handlers, indexable by handle + std::unordered_map on_clicks; + /// @brief all of the onHover handlers, indexable by handle + std::unordered_map on_hovers; + +private: + /// @brief internal counter to assign to the next onClick handler + CallbackHandle next_click_handle {0}; + /// @brief internal counter to assign to the next onHover handler + CallbackHandle next_hover_handle {0}; + + /// @brief helper function to determine if + bool _doesIntersect(float x, float y) const; +}; + + +} diff --git a/include/client/lightsource.hpp b/include/client/lightsource.hpp new file mode 100644 index 00000000..a7256107 --- /dev/null +++ b/include/client/lightsource.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include "client/core.hpp" +#include "client/shader.hpp" + +#define GLM_ENABLE_EXPERIMENTAL +#include +#include +#include + +#include +#include +#include +#include + +class LightSource { +public: + LightSource(); + void draw(std::shared_ptr shader, glm::mat4 viewProj); + void TranslateTo(const glm::vec3& new_pos); + + glm::vec3 lightPos; +private: + GLuint VAO, VBO; + + glm::mat4 model; + glm::vec3 color; + + // Cube Information + std::vector positions; + std::vector normals; + std::vector indices; + +}; diff --git a/include/client/model.hpp b/include/client/model.hpp new file mode 100644 index 00000000..27dcb9a5 --- /dev/null +++ b/include/client/model.hpp @@ -0,0 +1,172 @@ +#pragma once + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "assimp/material.h" +#include "client/renderable.hpp" +#include "client/shader.hpp" + +/** + * Stores position, normal vector, and coordinates + * in texture map for every vertex in a mesh. + */ +struct Vertex { + glm::vec3 position; + glm::vec3 normal; + glm::vec2 textureCoords; +}; + +class Texture { + public: + /** + * Load a texture from a filepath. + * + * @param filepath is the file path to the texture + * @param type specifies the type of texture to load. + * Currently only aiTextureType_SPECULAR and aiTextureType_DIFFUSE + * are implemented. + */ + Texture(const std::string& filepath, const aiTextureType& type); + + /** + * @return the texture's ID to be passed into OpenGL functions + */ + GLuint getID() const; + + /* + * Get the type of texture. Either "texture_diffuse" or "texture_specular". + */ + std::string getType() const; + private: + GLuint ID; + std::string type; +}; + +struct Material { + glm::vec3 ambient; + glm::vec3 diffuse; + glm::vec3 specular; + float shininess; +}; + +/** + * Mesh holds the data needed to render a mesh (collection of triangles). + * + * A Mesh differs from a Model since a Model is typically made up of multiple, + * smaller meshes. This is useful for animating parts of model individual (ex: legs, + * arms, head) + */ +class Mesh : public Renderable { + public: + /** + * Creates a new mesh from a collection of vertices, indices and textures + */ + Mesh( + const std::vector& vertices, + const std::vector& indices, + const std::vector& textures, + const Material& material + ); + + /** + * Render the Mesh to the viewport using the provided shader program. + * + * @param shader to use when rendering the program. Determines position of + * vertices and their color/texture. + * @param modelView determines the scaling/rotation/translation of the + * mesh + */ + void draw(std::shared_ptr shader, + glm::mat4 viewProj, + glm::vec3 camPos, + glm::vec3 lightPos, + bool fill) override; + private: + std::vector vertices; + std::vector indices; + std::vector textures; + Material material; + + // render data opengl needs + GLuint VAO, VBO, EBO; +}; + + +class Model : public Renderable { + public: + /** + * Loads Model from a given filename. Can be of format + * .obj, .blend or any of the formats that assimp supports + * @see https://assimp-docs.readthedocs.io/en/latest/about/introduction.html?highlight=obj#introduction + * + * @param Filepath to model file. + */ + explicit Model(const std::string& filepath); + + /** + * Draws all the meshes of a given model + * + * @param Shader to use while drawing all the + * meshes of the model + */ + void draw(std::shared_ptr shader, + glm::mat4 viewProj, + glm::vec3 camPos, + glm::vec3 lightPos, + bool fill) override; + + /** + * Sets the position of the Model to the given x,y,z + * values + * + * @param vector of x, y, z of the model's new position + */ + void translateAbsolute(const glm::vec3& new_pos) override; + + /** + * Updates the position of the Model relative to it's + * previous position + * + * @param vector of x, y, z of the change in the Model's + * position + */ + void translateRelative(const glm::vec3& delta) override; + + /** + * Scale the Model across all axes (x,y,z) + * by a factor + * + * @param new_factor describes how much to scale the model by. + * Ex: setting it to 0.5 will cut the model's rendered size + * in half. + */ + void scale(const float& new_factor) override; + + /** + * Scale the model across all axes (x,y,z) + * by the scale factor in each axis. + * + * @param the scale vector describes how much to independently scale + * the model in each axis (x, y, z) + */ + void scale(const glm::vec3& scale) override; + private: + std::vector meshes; + + void processNode(aiNode* node, const aiScene* scene); + Mesh processMesh(aiMesh* mesh, const aiScene* scene); + std::vector loadMaterialTextures(aiMaterial* mat, const aiTextureType& type); + + // store the directory of the model file so that textures can be + // loaded relative to the model file + std::string directory; +}; diff --git a/include/client/renderable.hpp b/include/client/renderable.hpp new file mode 100644 index 00000000..a06efa05 --- /dev/null +++ b/include/client/renderable.hpp @@ -0,0 +1,70 @@ +#pragma once + +#include + +#include + +#include "client/shader.hpp" + +class Renderable { + public: + Renderable(); + /** + * Draws the renderable item + * + * @param Shader to use while drawing all the + * meshes of the model + * @param + */ + virtual void draw(std::shared_ptr shader, + glm::mat4 viewProj, + glm::vec3 camPos, + glm::vec3 lightPos, + bool fill) = 0; + + /** + * Sets the position of the item to the given x,y,z + * values + * + * @param vector of x, y, z of the 's new position + */ + virtual void translateAbsolute(const glm::vec3& new_pos); + + /** + * Updates the position of the item relative to it's + * previous position + * + * @param vector of x, y, z of the change in the item's + * position + */ + virtual void translateRelative(const glm::vec3& delta); + + /** + * Scale the Model across all axes (x,y,z) + * by a factor + * + * @param new_factor describes how much to scale the model by. + * Ex: setting it to 0.5 will cut the model's rendered size + * in half. + */ + virtual void scale(const float& new_factor); + + /** + * Scale the item across all axes (x,y,z) + * by the scale factor in each axis. + * + * @param the scale vector describes how much to independently scale + * the item in each axis (x, y, z) + */ + virtual void scale(const glm::vec3& scale); + + /** + * Gets the model matrix given all the transformations + * applied to it + * + * @return updated model matrix + */ + glm::mat4 getModelMat(); + private: + glm::mat4 model; +}; diff --git a/include/client/shader.hpp b/include/client/shader.hpp index 2c5d1d46..17284347 100644 --- a/include/client/shader.hpp +++ b/include/client/shader.hpp @@ -1,5 +1,65 @@ #pragma once +#include +#include +#include +#include +#include +#include + -GLuint loadCubeShaders(); +class Shader { + public: + /** + * Create a shader program from filepaths to a vertex and fragment shader. + * Use getID() to access the shader program's ID. + */ + Shader(const std::string& vertexPath, const std::string& fragmentPath); + ~Shader(); + + /* + * @return the shader program's ID which can be passed into OpenGL functions + */ + GLuint getID(); + + /* + * Activate the shader program. Must be called before drawing. + * Calls glUseProgram under the hood. + */ + void use(); + + /* + * Sets a boolean unform variable of the shader program + * with the specified value + * @param name is the name of the uniform variable as written + * in the shader program + * @param value is the boolean value to write to that variable + */ + void setBool(const std::string &name, bool value) const; + + /* + * Sets an integer unform variable of the shader program + * with the specified value + * @param name is the name of the uniform variable as written + * in the shader program + * @param value is the integer value to write to that variable + */ + void setInt(const std::string &name, int value) const; + + /* + * Sets a float unform variable of the shader program + * with the specified value + * @param name is the name of the uniform variable as written + * in the shader program + * @param value is the float value to write to that variable + */ + void setFloat(const std::string &name, float value) const; + void setMat4(const std::string &name, glm::mat4& value); + void setVec3(const std::string &name, glm::vec3& value); + + glm::vec3 getVec3(const std::string &name); + private: + // the shader program ID + unsigned int ID; +}; diff --git a/include/client/util.hpp b/include/client/util.hpp index e9c4ffaa..1206e4fc 100644 --- a/include/client/util.hpp +++ b/include/client/util.hpp @@ -1,19 +1,7 @@ #pragma once -#include +#include "assimp/types.h" +#include -#include -#include -#include -#include -#include +glm::vec3 aiColorToGLM(const aiColor3D& color); -// #include - -// #include -// #include - -#include "client/core.hpp" - - -GLuint LoadShaders(const std::string& vertex_file_path, const std::string& fragment_file_path); diff --git a/include/server/game/gridcell.hpp b/include/server/game/gridcell.hpp index 02e236d2..b0c29072 100644 --- a/include/server/game/gridcell.hpp +++ b/include/server/game/gridcell.hpp @@ -4,6 +4,7 @@ enum class CellType { Empty, Wall, Spawn, + Enemy, Unknown }; diff --git a/include/server/game/servergamestate.hpp b/include/server/game/servergamestate.hpp index c3ccc41d..97e25043 100644 --- a/include/server/game/servergamestate.hpp +++ b/include/server/game/servergamestate.hpp @@ -48,7 +48,7 @@ class ServerGameState { */ explicit ServerGameState(GamePhase start_phase); - ServerGameState(GamePhase start_phase, GameConfig config); + ServerGameState(GamePhase start_phase, const GameConfig& config); /** * @brief This is the ONLY constructor that initializes the maze from a @@ -123,14 +123,8 @@ class ServerGameState { * Removes a player from the lobby with the specified id. */ void removePlayerFromLobby(EntityID id); - /** - * Getter for the mapping between entity ID and player name in the lobby - */ - const std::unordered_map& getLobbyPlayers() const; - /** - * Returns how many max players can be in the lobby, based on the config option - */ - int getLobbyMaxPlayers() const; + + const Lobby& getLobby() const; /* Maze initialization */ diff --git a/include/server/lobbybroadcaster.hpp b/include/server/lobbybroadcaster.hpp index 3da9e9db..66b9e9bb 100644 --- a/include/server/lobbybroadcaster.hpp +++ b/include/server/lobbybroadcaster.hpp @@ -37,7 +37,7 @@ class LobbyBroadcaster { * Tell the broadcaster the current state of the lobby so it can broadcast * updated messages. */ - void setLobbyInfo(const ServerLobbyBroadcastPacket& bcast_info); + void setLobbyInfo(const Lobby& bcast_info); /** * Tells the LobbyBroadcaster to stop advertising that there is a lobby open. diff --git a/include/shared/game/sharedgamestate.hpp b/include/shared/game/sharedgamestate.hpp index d59f972a..65c11c70 100644 --- a/include/shared/game/sharedgamestate.hpp +++ b/include/shared/game/sharedgamestate.hpp @@ -1,5 +1,11 @@ #pragma once +#include +#include +#include +#include +#include + //#include "server/game/servergamestate.hpp" #include "shared/game/sharedobject.hpp" #include "shared/utilities/smartvector.hpp" @@ -7,9 +13,6 @@ #include "shared/utilities/config.hpp" #include "server/game/constants.hpp" -#include -#include -#include enum class GamePhase { TITLE_SCREEN, @@ -21,9 +24,10 @@ enum class GamePhase { * @brief Information about the current lobby of players. */ struct Lobby { - // TODO: Perhaps instead of a mapping from EntityID -> string, the mapping - // could eventually be EntityID -> Player (where Player derives from - // Object)? + /** + * @brief name of the lobby as set by the server + */ + std::string name; /** * @brief A hash table that maps from player's EntityID to their names. @@ -35,6 +39,11 @@ struct Lobby { */ int max_players; + + DEF_SERIALIZE(Archive& ar, unsigned int version) { + ar & name & players & max_players; + } + // TODO: Add a player role listing? I.e., which player is playing which // character and which player is playing as the Dungeon Master? }; @@ -64,16 +73,17 @@ struct SharedGameState { this->lobby.max_players = MAX_PLAYERS; } - SharedGameState(GamePhase start_phase, GameConfig config): + SharedGameState(GamePhase start_phase, const GameConfig& config): objects(std::vector>()) { this->phase = start_phase; this->timestep = FIRST_TIMESTEP; this->timestep_length = config.game.timestep_length_ms; this->lobby.max_players = config.server.max_players; + this->lobby.name = config.server.lobby_name; } DEF_SERIALIZE(Archive& ar, const unsigned int version) { - ar & phase& lobby.max_players & lobby.players & objects; + ar & phase & lobby & objects; } }; \ No newline at end of file diff --git a/include/shared/utilities/config.hpp b/include/shared/utilities/config.hpp index a7914f03..15d4ebe2 100644 --- a/include/shared/utilities/config.hpp +++ b/include/shared/utilities/config.hpp @@ -50,6 +50,7 @@ struct GameConfig { std::string default_name; /// @brief Whether or not the client should listen for server lobby broadcasts bool lobby_discovery; + int window_width; } client; /** diff --git a/include/shared/utilities/serialize_macro.hpp b/include/shared/utilities/serialize_macro.hpp index ddd269cc..9c9863f8 100644 --- a/include/shared/utilities/serialize_macro.hpp +++ b/include/shared/utilities/serialize_macro.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include diff --git a/include/shared/utilities/time.hpp b/include/shared/utilities/time.hpp new file mode 100644 index 00000000..fc13a7b2 --- /dev/null +++ b/include/shared/utilities/time.hpp @@ -0,0 +1,3 @@ +#pragma once + +long getMsSinceEpoch(); \ No newline at end of file diff --git a/maps/default_maze.maze b/maps/default_maze.maze index 945c9b46..b516b2c4 100644 --- a/maps/default_maze.maze +++ b/maps/default_maze.maze @@ -1 +1 @@ -. \ No newline at end of file +@ \ No newline at end of file diff --git a/maps/maze5.maze b/maps/maze5.maze index e87094c9..2b32698b 100644 --- a/maps/maze5.maze +++ b/maps/maze5.maze @@ -1,5 +1,5 @@ ############################# -#@......#...#@............#.# +#@......#...#@............#E# #######...#.#...@...##.##.#.# #.......###.........#.@.#...# ############################# \ No newline at end of file diff --git a/shell.nix b/shell.nix index 3d6f66bb..be17b5a5 100644 --- a/shell.nix +++ b/shell.nix @@ -9,6 +9,7 @@ mkShell { gnumake gcc13 gdb + zlib wayland wayland-scanner @@ -34,6 +35,8 @@ mkShell { doxygen clang-tools_14 cppcheck + + python310Packages.gdown ]; nativeBuildInputs = with pkgs; [ pkg-config diff --git a/src/client/CMakeLists.txt b/src/client/CMakeLists.txt index 5ba10bae..6f6a72b1 100644 --- a/src/client/CMakeLists.txt +++ b/src/client/CMakeLists.txt @@ -1,6 +1,18 @@ set(TARGET_NAME client) set(LIB_NAME game_client_lib) +# Subprojects +# potential review later +add_subdirectory(../../dependencies/glfw ${CMAKE_BINARY_DIR}/glfw) +add_subdirectory(../../dependencies/glm ${CMAKE_BINARY_DIR}/glm) +add_subdirectory(../../dependencies/glew ${CMAKE_BINARY_DIR}/glew) +add_subdirectory(../../dependencies/freetype ${CMAKE_BINARY_DIR}/freetype) +add_subdirectory(../../dependencies/sfml ${CMAKE_BINARY_DIR}/sfml) +add_subdirectory(../../dependencies/assimp ${CMAKE_BINARY_DIR}/assimp) +add_subdirectory(../../dependencies/stb ${CMAKE_BINARY_DIR}/stb) + +add_subdirectory(gui) + set(FILES sound.cpp camera.cpp @@ -8,54 +20,74 @@ set(FILES cube.cpp util.cpp lobbyfinder.cpp - shaders.cpp - ${imgui-source} + + shader.cpp + model.cpp + lightsource.cpp + renderable.cpp ) # OpenGL set(OpenGL_GL_PREFERENCE GLVND) find_package(OpenGL REQUIRED) -# Subprojects -# potential review later -add_subdirectory(../../dependencies/glfw ${CMAKE_BINARY_DIR}/glfw) -add_subdirectory(../../dependencies/glm ${CMAKE_BINARY_DIR}/glm) -add_subdirectory(../../dependencies/imgui ${CMAKE_BINARY_DIR}/imgui) -add_subdirectory(../../dependencies/glew ${CMAKE_BINARY_DIR}/glew) -add_subdirectory(../../dependencies/sfml ${CMAKE_BINARY_DIR}/sfml) - add_library(${LIB_NAME} STATIC ${FILES}) target_include_directories(${LIB_NAME} PRIVATE ${INCLUDE_DIRECTORY}) -target_include_directories(${LIB_NAME} PRIVATE ${BOOST_LIBRARY_INCLUDES}) +target_include_directories(${LIB_NAME} + PRIVATE + ${BOOST_LIBRARY_INCLUDES} + ${OPENGL_INCLUDE_DIRS} + ${FREETYPE_INCLUDE_DIRS} + glfw + glm + ${GLEW_INCLUDE_DIRS} + ${ASSIMP_INCLUDE_DIRS} + ${STB_INCLUDE_DIRS} +) target_link_libraries(${LIB_NAME} PRIVATE game_shared_lib + game_gui_lib Boost::asio Boost::filesystem Boost::thread Boost::program_options Boost::serialization nlohmann_json::nlohmann_json + glm + glfw + libglew_static + assimp + freetype ) -target_include_directories(${LIB_NAME} PRIVATE ${OPENGL_INCLUDE_DIRS} glfw glm ${imgui-directory} "${CMAKE_BINARY_DIR}/_deps/glew-src/include") -target_link_libraries(${LIB_NAME} PRIVATE glm glfw libglew_static) add_executable(${TARGET_NAME} main.cpp) - target_include_directories(${TARGET_NAME} PRIVATE ${INCLUDE_DIRECTORY}) -target_include_directories(${TARGET_NAME} PRIVATE ${OPENGL_INCLUDE_DIRS} glfw glm ${imgui-directory} "${CMAKE_BINARY_DIR}/_deps/glew-src/include") +target_include_directories(${TARGET_NAME} + PRIVATE + ${BOOST_LIBRARY_INCLUDES} + ${OPENGL_INCLUDE_DIRS} + ${FREETYPE_INCLUDE_DIRS} + glfw + glm + ${GLEW_INCLUDE_DIRS} + ${ASSIMP_INCLUDE_DIRS} + ${STB_INCLUDE_DIRS} +) target_link_libraries(${TARGET_NAME} PRIVATE game_shared_lib ${LIB_NAME}) -target_link_libraries(${TARGET_NAME} PRIVATE glm glfw libglew_static) - -target_include_directories(${TARGET_NAME} PRIVATE ${BOOST_LIBRARY_INCLUDES}) -target_link_libraries(${TARGET_NAME} - PRIVATE +target_link_libraries(${TARGET_NAME} + PRIVATE Boost::asio Boost::filesystem Boost::thread Boost::program_options Boost::serialization nlohmann_json::nlohmann_json + glm + glfw + libglew_static + assimp + freetype ) target_link_libraries(${TARGET_NAME} PRIVATE sfml-audio) diff --git a/src/client/camera.cpp b/src/client/camera.cpp index 11175ebf..07fa3ca9 100644 --- a/src/client/camera.cpp +++ b/src/client/camera.cpp @@ -35,6 +35,10 @@ glm::mat4 Camera::getViewProj() { return viewProjMat; } +glm::vec3 Camera::getPos() { + return cameraPos; +} + void Camera::update(float xpos, float ypos) { if (firstMouse) diff --git a/src/client/client.cpp b/src/client/client.cpp index 68db4356..2dbc2651 100644 --- a/src/client/client.cpp +++ b/src/client/client.cpp @@ -1,5 +1,4 @@ #include "client/client.hpp" - #include #include @@ -7,13 +6,28 @@ #include #include #include + +#include +#include +#include + +#include "client/gui/gui.hpp" +#include "client/constants.hpp" #include +#include "client/lightsource.hpp" #include "client/shader.hpp" +#include "client/model.hpp" +#include "glm/fwd.hpp" +#include "server/game/solidsurface.hpp" #include "shared/game/event.hpp" +#include "shared/game/sharedobject.hpp" #include "shared/network/constants.hpp" #include "shared/network/packet.hpp" #include "shared/utilities/config.hpp" +#include "shared/utilities/root_path.hpp" +#include "shared/utilities/time.hpp" + using namespace boost::asio::ip; using namespace std::chrono_literals; @@ -35,22 +49,35 @@ bool Client::cam_is_held_up = false; bool Client::cam_is_held_down = false; bool Client::cam_is_held_right = false; bool Client::cam_is_held_left = false; +bool Client::is_left_mouse_down = false; float Client::mouse_xpos = 0.0f; float Client::mouse_ypos = 0.0f; -Client::Client(boost::asio::io_context& io_context, GameConfig config) : +int Client::window_width = UNIT_WINDOW_WIDTH; +int Client::window_height = UNIT_WINDOW_HEIGHT; + +time_t Client::time_of_last_keystroke = 0; + +using namespace gui; + +Client::Client(boost::asio::io_context& io_context, GameConfig config): resolver(io_context), socket(io_context), config(config), - gameState(GamePhase::TITLE_SCREEN, config) + gameState(GamePhase::TITLE_SCREEN, config), + session(nullptr), + gui(this), + gui_state(gui::GUIState::INITIAL_LOAD), + lobby_finder(io_context, config) { cam = new Camera(); - this->root_path = boost::dll::program_location().parent_path().parent_path().parent_path(); -} - -boost::filesystem::path Client::getRootPath() { - return this->root_path; + Client::window_width = config.client.window_width; + Client::window_height = static_cast((config.client.window_width * 2.0f) / 3.0f); + + if (config.client.lobby_discovery) { + lobby_finder.startSearching(); + } } void Client::connectAndListen(std::string ip_addr) { @@ -60,8 +87,13 @@ void Client::connectAndListen(std::string ip_addr) { this->session->connectTo(this->endpoints); + auto name = this->gui.getCapturedKeyboardInput(); + if (name == "") { + name = config.client.default_name; + } + auto packet = PackagedPacket::make_shared(PacketType::ClientDeclareInfo, - ClientDeclareInfoPacket { .player_name = config.client.default_name }); + ClientDeclareInfoPacket { .player_name = name }); this->session->sendPacketAsync(packet); @@ -75,44 +107,77 @@ Client::~Client() { // TODO: error flags / output for broken init bool Client::init() { /* Initialize glfw library */ - if (!glfwInit()) + if (!glfwInit()) { + const char* glfwErrorDesc = NULL; + glfwGetError(&glfwErrorDesc); + std::cout << "glfw init fails" << glfwErrorDesc << std::endl; return false; + } /* Create a windowed mode window and its OpenGL context */ glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); - window = glfwCreateWindow(640, 480, "Hello World", NULL, NULL); - if (!window) { + window = glfwCreateWindow(Client::window_width, Client::window_height, "Arcana", NULL, NULL); + if (!window) + { glfwTerminate(); return false; } + glfwSetWindowSizeLimits(window, + Client::window_width, Client::window_height, Client::window_width, Client::window_height); /* Make the window's context current */ glfwMakeContextCurrent(window); - // tell GLFW to capture our mouse - glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED); - - GLenum err = glewInit(); - if (GLEW_OK != err) { - std::cerr << "Error loading GLEW: " << glewGetString(err) << std::endl; + GLenum err = glewInit() ; + if (GLEW_OK != err) { + std::cerr << "Error loading GLEW: " << glewGetString(err) << std::endl; return false; } std::cout << "shader version: " << glGetString(GL_SHADING_LANGUAGE_VERSION) << std::endl; std::cout << "shader version: " << glGetString(GL_VERSION) << std::endl; - this->cubeShaderProgram = loadCubeShaders(); - if (!this->cubeShaderProgram) { - std::cout << "Failed to load cube shader files" << std::endl; + // Init GUI (e.g. load in all fonts) + // TODO: pass in shader for image loading in second param + if (!this->gui.init()) { + std::cerr << "GUI failed to init" << std::endl; return false; } + this->displayCallback(); + + auto shaders_dir = getRepoRoot() / "src/client/shaders"; + auto graphics_assets_dir = getRepoRoot() / "assets/graphics"; + + auto cube_vert_path = shaders_dir / "cube.vert"; + auto cube_frag_path = shaders_dir / "cube.frag"; + this->cube_shader = std::make_shared(cube_vert_path.string(), cube_frag_path.string()); + + auto model_vert_path = shaders_dir / "model.vert"; + auto model_frag_path = shaders_dir / "model.frag"; + this->model_shader = std::make_shared(model_vert_path.string(), model_frag_path.string()); + + auto bear_model_path = graphics_assets_dir / "bear-sp22.obj"; + this->bear_model = std::make_unique(bear_model_path.string()); + this->bear_model->scale(0.25); + + auto player_model_path = graphics_assets_dir / "Fire-testing.obj"; + this->player_model = std::make_unique(player_model_path.string()); + this->player_model->scale(0.25); + + this->light_source = std::make_unique(); + + auto lightVertFilepath = shaders_dir / "lightsource.vert"; + auto lightFragFilepath = shaders_dir / "lightsource.frag"; + this->light_source_shader = std::make_shared(lightVertFilepath.string(), lightFragFilepath.string()); + + this->gui_state = GUIState::TITLE_SCREEN; + return true; } bool Client::cleanup() { - glDeleteProgram(this->cubeShaderProgram); return true; } @@ -121,10 +186,19 @@ void Client::displayCallback() { /* Render here */ glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); - if (this->gameState.phase == GamePhase::GAME) { + this->gui.beginFrame(); + + if (this->gameState.phase == GamePhase::TITLE_SCREEN) { + + } else if (this->gameState.phase == GamePhase::LOBBY) { + } else if (this->gameState.phase == GamePhase::GAME) { this->draw(); } + this->gui.layoutFrame(this->gui_state); + this->gui.handleInputs(mouse_xpos, mouse_ypos, is_left_mouse_down); + this->gui.renderFrame(); + /* Poll for and process events */ glfwPollEvents(); glfwSwapBuffers(window); @@ -132,6 +206,14 @@ void Client::displayCallback() { // Handle any updates void Client::idleCallback(boost::asio::io_context& context) { + if (this->session != nullptr) { + processServerInput(context); + } + + // If we aren't in the middle of the game then we shouldn't capture any movement info + // or send any movement related events + if (this->gui_state != GUIState::GAME_HUD) { return; } + std::optional jump = glm::vec3(0.0f); std::optional cam_movement = glm::vec3(0.0f); @@ -150,7 +232,7 @@ void Client::idleCallback(boost::asio::io_context& context) { cam->update(mouse_xpos, mouse_ypos); // IF PLAYER, allow moving - if (this->session->getInfo().client_eid.has_value()) { + if (this->session != nullptr && this->session->getInfo().client_eid.has_value()) { auto eid = this->session->getInfo().client_eid.value(); this->session->sendEventAsync(Event(eid, EventType::ChangeFacing, ChangeFacingEvent(eid, cam_movement.value()))); @@ -174,8 +256,6 @@ void Client::idleCallback(boost::asio::io_context& context) { sentCamMovement = cam_movement.value(); } } - - processServerInput(context); } // Handles given key @@ -210,7 +290,13 @@ void Client::processServerInput(boost::asio::io_context& context) { for (Event event : this->session->getEvents()) { if (event.type == EventType::LoadGameState) { + GamePhase old_phase = this->gameState.phase; this->gameState = boost::get(event.data).state; + + // Change the UI to the game hud UI whenever we change into the GAME game phase + if (old_phase != GamePhase::GAME && this->gameState.phase == GamePhase::GAME) { + this->gui_state = GUIState::GAME_HUD; + } } } } @@ -221,50 +307,100 @@ void Client::draw() { for (int i = 0; i < this->gameState.objects.size(); i++) { std::shared_ptr sharedObject = this->gameState.objects.at(i); - if (sharedObject == nullptr) - continue; - - std::cout << "shared object " << i << ": position: " << glm::to_string(sharedObject->physics.position) << std::endl; - - // Get camera position from server, update position and don't render player object (or special handling) - if (this->session->getInfo().client_eid.has_value() && sharedObject->globalID == this->session->getInfo().client_eid.value()) { - glm::vec3 pos = sharedObject->physics.position; - pos.y += 1.5f; - cam->updatePos(pos); - - Cube* cube = new Cube(glm::vec3(0.0f, 0.5f, 1.0f), glm::vec3(1.0f)); - cube->update(sharedObject->physics.position); - cube->draw(this->cam->getViewProj(), this->cubeShaderProgram, true); + if (sharedObject == nullptr) { continue; } - // If solidsurface, scale cube to given dimensions - if (sharedObject->solidSurface.has_value()) { - Cube* cube = new Cube(glm::vec3(0.4f, 0.5f, 0.7f), sharedObject->solidSurface->dimensions); - cube->update(sharedObject->physics.position); - cube->draw(this->cam->getViewProj(), this->cubeShaderProgram, true); - continue; + switch (sharedObject->type) { + case ObjectType::Player: { + // don't render yourself + if (this->session->getInfo().client_eid.has_value() && sharedObject->globalID == this->session->getInfo().client_eid.value()) { + glm::vec3 pos = sharedObject->physics.position; + pos.y += PLAYER_EYE_LEVEL; + cam->updatePos(pos); + break; + } + auto lightPos = glm::vec3(-5.0f, 0.0f, 0.0f); + // subtracting 1 from y position to render players "standing" on ground + auto player_pos = glm::vec3(sharedObject->physics.position.x, sharedObject->physics.position.y - 1.0f, sharedObject->physics.position.z); + + this->player_model->translateAbsolute(player_pos); + this->player_model->draw( + this->model_shader, + this->cam->getViewProj(), + this->cam->getPos(), + lightPos, + true); + break; + } + case ObjectType::Enemy: { + // warren bear is an enemy because why not + // auto pos = glm::vec3(0.0f, 0.0f, 0.0f); + auto lightPos = glm::vec3(-5.0f, 0.0f, 0.0f); + this->bear_model->translateAbsolute(sharedObject->physics.position); + this->bear_model->draw( + this->model_shader, + this->cam->getViewProj(), + this->cam->getPos(), + lightPos, + true); + + /* this->light_source->TranslateTo(lightPos); + this->light_source->draw( + this->light_source_shader, + this->cam->getViewProj());*/ + + // Cube* cube = new Cube(glm::vec3(0.4f,0.5f,0.7f)); + // cube->translateAbsolute(lightPos); + // cube->draw(this->cube_shader, + // this->cam->getViewProj(), + // this->cam->getPos(), + // glm::vec3(), + // false); + break; + } + case ObjectType::SolidSurface: { + Cube* cube = new Cube(glm::vec3(0.4f,0.5f,0.7f)); + cube->scale( sharedObject->solidSurface->dimensions); + cube->translateAbsolute(sharedObject->physics.position); + cube->draw(this->cube_shader, + this->cam->getViewProj(), + this->cam->getPos(), + glm::vec3(), + true); + break; + } + default: + break; } - - /* - Cube* cube = new Cube(glm::vec3(0.0f, 1.0f, 1.0f), glm::vec3(8.0f, 8.0f, 3.0f)); - cube->update(sharedObject->physics.position); - cube->draw(this->cam->getViewProj(), this->cubeShaderProgram, false);*/ - - Cube* origin = new Cube(glm::vec3(1.0f, 0.0f, 0.0f), glm::vec3(0.05f, 0.05f, 0.05f)); - origin->update(glm::vec3(0.0f)); - origin->draw(this->cam->getViewProj(), this->cubeShaderProgram, true); } } // callbacks - for Interaction -void Client::keyCallback(GLFWwindow* window, int key, int scancode, int action, int mods) { +void Client::keyCallback(GLFWwindow *window, int key, int scancode, int action, int mods) { + Client* client = static_cast(glfwGetWindowUserPointer(window)); + // Check for a key press. if (action == GLFW_PRESS) { switch (key) { case GLFW_KEY_ESCAPE: - // Close the window. This causes the program to also terminate. - glfwSetWindowShouldClose(window, GL_TRUE); + if (client->gameState.phase == GamePhase::GAME) { + if (client->gui_state == GUIState::GAME_ESC_MENU) { + client->gui_state = GUIState::GAME_HUD; + } else if (client->gui_state == GUIState::GAME_HUD) { + client->gui_state = GUIState::GAME_ESC_MENU; + } + } + client->gui.setCaptureKeystrokes(false); + break; + + case GLFW_KEY_TAB: + client->gui.setCaptureKeystrokes(true); + break; + + case GLFW_KEY_BACKSPACE: + client->gui.captureBackspace(); + Client::time_of_last_keystroke = getMsSinceEpoch(); break; case GLFW_KEY_DOWN: @@ -358,9 +494,44 @@ void Client::keyCallback(GLFWwindow* window, int key, int scancode, int action, break; } } + + if (action == GLFW_REPEAT) { + if (key == GLFW_KEY_BACKSPACE) { + auto ms_since_epoch = getMsSinceEpoch(); + if (Client::time_of_last_keystroke + 100 < ms_since_epoch) { + Client::time_of_last_keystroke = ms_since_epoch; + client->gui.captureBackspace(); + } + } + } } void Client::mouseCallback(GLFWwindow* window, double xposIn, double yposIn) { // cppcheck-suppress constParameterPointer mouse_xpos = static_cast(xposIn); mouse_ypos = static_cast(yposIn); +} + +void Client::mouseButtonCallback(GLFWwindow* window, int button, int action, int mods) { + if (button == GLFW_MOUSE_BUTTON_LEFT) { + if (action == GLFW_PRESS) { + is_left_mouse_down = true; + } else if (action == GLFW_RELEASE) { + is_left_mouse_down = false; + } + } +} + +void Client::charCallback(GLFWwindow* window, unsigned int codepoint) { + Client* client = static_cast(glfwGetWindowUserPointer(window)); + + client->gui.captureKeystroke(static_cast(codepoint)); + Client::time_of_last_keystroke = getMsSinceEpoch(); +} + +glm::vec2 Client::getWindowSize() { + return glm::vec2(Client::window_width, Client::window_height); +} + +time_t Client::getTimeOfLastKeystroke() { + return Client::time_of_last_keystroke; } \ No newline at end of file diff --git a/src/client/cube.cpp b/src/client/cube.cpp index 4a0438d8..ef0a295e 100644 --- a/src/client/cube.cpp +++ b/src/client/cube.cpp @@ -1,16 +1,12 @@ #include "client/cube.hpp" -Cube::Cube(glm::vec3 newColor, glm::vec3 scale) { +Cube::Cube(glm::vec3 newColor) { // create a vertex buffer for positions and normals // insert the data into these buffers // initialize model matrix - // Model matrix. glm::vec3 cubeMin = glm::vec3(-0.5f, -0.5f, -0.5f); glm::vec3 cubeMax = glm::vec3(0.5f, 0.5f, 0.5f); - model = glm::mat4(1.0f); - //scale the cube to with given vector - model = glm::scale(model, scale); // The color of the cube. Try setting it to something else! color = newColor; @@ -139,15 +135,20 @@ Cube::~Cube() { glDeleteVertexArrays(1, &VAO); } -void Cube::draw(glm::mat4 viewProjMat, GLuint shader, bool fill) { - // actiavte the shader program - glUseProgram(shader); +void Cube::draw(std::shared_ptr shader, + glm::mat4 viewProj, + glm::vec3 camPos, + glm::vec3 lightPos, + bool fill) { + // actiavte the shader program + shader->use(); // get the locations and send the uniforms to the shader - glUniformMatrix4fv(glGetUniformLocation(shader, "viewProj"), 1, false, reinterpret_cast(&viewProjMat)); - glUniformMatrix4fv(glGetUniformLocation(shader, "model"), 1, GL_FALSE, reinterpret_cast(&model)); - glUniform3fv(glGetUniformLocation(shader, "DiffuseColor"), 1, &color[0]); + shader->setMat4("viewProj", viewProj); + auto model = this->getModelMat(); + shader->setMat4("model", model); + shader->setVec3("DiffuseColor", color); // Bind the VAO glBindVertexArray(VAO); @@ -166,11 +167,3 @@ void Cube::draw(glm::mat4 viewProjMat, GLuint shader, bool fill) { glBindVertexArray(0); glUseProgram(0); } - -void Cube::update(glm::vec3 new_pos) { - model[3] = glm::vec4(new_pos, 1.0f); -} - -void Cube::update_delta(glm::vec3 delta) { - model = glm::translate(model, delta); -} \ No newline at end of file diff --git a/src/client/gui/CMakeLists.txt b/src/client/gui/CMakeLists.txt new file mode 100644 index 00000000..fa19449a --- /dev/null +++ b/src/client/gui/CMakeLists.txt @@ -0,0 +1,47 @@ +set(LIB_NAME game_gui_lib) + +set(FILES + imgs/img.cpp + imgs/loader.cpp + + font/font.cpp + font/loader.cpp + + widget/centertext.cpp + widget/textinput.cpp + widget/widget.cpp + widget/dyntext.cpp + widget/flexbox.cpp + widget/staticimg.cpp + + gui.cpp +) + +# OpenGL +set(OpenGL_GL_PREFERENCE GLVND) +find_package(OpenGL REQUIRED) + +add_library(${LIB_NAME} STATIC ${FILES}) +target_include_directories(${LIB_NAME} PRIVATE ${INCLUDE_DIRECTORY}) +target_link_libraries(${LIB_NAME} PRIVATE game_shared_lib) +target_include_directories(${LIB_NAME} PRIVATE + ${OPENGL_INCLUDE_DIRS} + ${BOOST_LIBRARY_INCLUDES} + ${STB_INCLUDE_DIRS} + glfw + glm + ${FREETYPE_INCLUDE_DIRS} + ${ASSIMP_INCLUDE_DIRS} + "${CMAKE_BINARY_DIR}/_deps/glew-src/include" +) +target_link_libraries(${LIB_NAME} PRIVATE glm glfw libglew_static freetype) +target_link_libraries(${LIB_NAME} + PRIVATE + Boost::asio + Boost::filesystem + Boost::thread + Boost::program_options + Boost::serialization + nlohmann_json::nlohmann_json + assimp +) \ No newline at end of file diff --git a/src/client/gui/font/font.cpp b/src/client/gui/font/font.cpp new file mode 100644 index 00000000..97af364d --- /dev/null +++ b/src/client/gui/font/font.cpp @@ -0,0 +1,48 @@ +#include "client/gui/font/font.hpp" + +#include "client/core.hpp" +#include "client/constants.hpp" +#include "client/client.hpp" +#include "shared/utilities/root_path.hpp" + +namespace gui::font { + +int getFontSizePx(Size size) { + return UNIT_LARGE_SIZE_PX * getScaleFactor(size); +} + +float getScaleFactor(Size size) { + float screen_factor = static_cast(WINDOW_WIDTH) / static_cast(UNIT_WINDOW_WIDTH); + + return SIZE_TO_SCALE.at(size) * screen_factor; +} + +std::string getFilepath(Font font) { + auto dir = getRepoRoot() / "assets/fonts"; + switch (font) { + case Font::MENU: return (dir / "AncientModernTales-a7Po.ttf").string(); + default: + case Font::TEXT: return (dir / "AncientModernTales-a7Po.ttf").string(); + } +} + +glm::vec3 getRGB(Color color) { + switch (color) { + case Color::RED: + return {1.0f, 0.0f, 0.0f}; + case Color::BLUE: + return {0.0f, 0.0f, 1.0f}; + case Color::GRAY: + return {0.5f, 0.5f, 0.5f}; + case Color::WHITE: + return {1.0f, 1.0f, 1.0f}; + case Color::TORCHLIGHT_GAMES: + return {0.902, 0.575, 0.055}; + + case Color::BLACK: + default: + return {0.0f, 0.0f, 0.0f}; + } +} + +} diff --git a/src/client/gui/font/loader.cpp b/src/client/gui/font/loader.cpp new file mode 100644 index 00000000..daa43b9c --- /dev/null +++ b/src/client/gui/font/loader.cpp @@ -0,0 +1,102 @@ +#include "client/gui/font/loader.hpp" + +#include + +// freetype needs this extra include for whatever unholy reason +#include +#include FT_FREETYPE_H + +#include "shared/utilities/root_path.hpp" + +namespace gui::font { + +bool Loader::init() { + if (FT_Init_FreeType(&this->ft)) { + std::cerr << "ERROR::FREETYPE: Could not init FreeType Library" << std::endl; + return false; + } + + // we mess with some alignment when creating the textures, + // so this is supposed to prevent seg faults related to that + glPixelStorei(GL_UNPACK_ALIGNMENT, 1); + + if (!this->_loadFont(Font::MENU)) { + return false; + } + if (!this->_loadFont(Font::TEXT)) { + return false; + } + + FT_Done_FreeType(this->ft); // done loading fonts, so can release these resources + + return true; +} + +const Character& Loader::loadChar(char c, Font font) const { + auto& char_map = this->font_map.at(font); + + if (!char_map.contains(c)) { + return char_map.at('?'); + } + + return char_map.at(c); +} + +bool Loader::_loadFont(Font font) { + auto path = font::getFilepath(font); + + std::cout << "Loading font: " << path << "\n"; + + FT_Face face; + if (FT_New_Face(this->ft, path.c_str(), 0, &face)) { + std::cout << "ERROR::FREETYPE: Failed to load font at " << path << std::endl; + return false; + } + + FT_Set_Pixel_Sizes(face, 0, UNIT_LARGE_SIZE_PX); + std::unordered_map characters; + for (unsigned char c = 0; c < 128; c++) { + // load character glyph + if (FT_Load_Char(face, c, FT_LOAD_RENDER)) { + std::cerr << "ERROR::FREETYTPE: Failed to load Glyph " << c << std::endl; + return false; + } + + // generate texture + unsigned int texture; + glGenTextures(1, &texture); + glBindTexture(GL_TEXTURE_2D, texture); + glTexImage2D( + GL_TEXTURE_2D, + 0, + GL_RED, + face->glyph->bitmap.width, + face->glyph->bitmap.rows, + 0, + GL_RED, + GL_UNSIGNED_BYTE, + face->glyph->bitmap.buffer + ); + // set texture options + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + // now store character for later use + Character character = { + texture, + glm::ivec2(face->glyph->bitmap.width, face->glyph->bitmap.rows), + glm::ivec2(face->glyph->bitmap_left, face->glyph->bitmap_top), + static_cast(face->glyph->advance.x) + }; + characters.insert({c, character}); + } + + this->font_map.insert({font, characters}); + + FT_Done_Face(face); + + return true; +} + +} diff --git a/src/client/gui/gui.cpp b/src/client/gui/gui.cpp new file mode 100644 index 00000000..b9b8169d --- /dev/null +++ b/src/client/gui/gui.cpp @@ -0,0 +1,358 @@ +#include "client/gui/gui.hpp" + +#include +#include +#include +#include "shared/utilities/rng.hpp" +#include "client/client.hpp" +#include "shared/game/sharedgamestate.hpp" + +namespace gui { + + +GUI::GUI(Client* client): capture_keystrokes(false) { + this->client = client; +} + +bool GUI::init() +{ + std::cout << "Initializing GUI...\n"; + + auto shader_path = getRepoRoot() / "src" / "client" / "shaders"; + + widget::DynText::shader = std::make_unique( + (shader_path / "text.vert").string(), (shader_path / "text.frag").string()); + if (widget::DynText::shader->getID() == 0) { + return false; + } + + widget::StaticImg::shader = std::make_unique( + (shader_path / "img.vert").string(), (shader_path / "img.frag").string()); + if (widget::StaticImg::shader->getID() == 0) { + return false; + } + + this->fonts = std::make_shared(); + this->capture_keystrokes = false; + + if (!this->fonts->init()) { + return false; + } + + if (!this->images.init()) { + return false; + } + + std::cout << "Initialized GUI\n"; + return true; +} + +void GUI::beginFrame() { + std::unordered_map empty; + std::swap(this->widgets, empty); +} + +void GUI::renderFrame() { + // for text rendering + glEnable(GL_TEXTURE_2D); + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); + + for (auto& [handle, widget] : this->widgets) { + widget->render(); + } + + glDisable(GL_BLEND); +} + +void GUI::handleInputs(float mouse_xpos, float mouse_ypos, bool& is_left_mouse_down) { + // convert to gui coords, where (0,0) is bottome left + mouse_ypos = WINDOW_HEIGHT - mouse_ypos; + if (is_left_mouse_down) { + this->_handleClick(mouse_xpos, mouse_ypos); + is_left_mouse_down = false; + } + this->_handleHover(mouse_xpos, mouse_ypos); +} + +widget::Handle GUI::addWidget(widget::Widget::Ptr&& widget) { + widget::Handle handle = this->next_handle++; + this->widgets.insert({handle, std::move(widget)}); + return handle; +} + +widget::Widget::Ptr GUI::removeWidget(widget::Handle handle) { + auto widget = std::move(this->widgets.at(handle)); + this->widgets.erase(handle); + return widget; +} + +void GUI::captureBackspace() { + if (this->shouldCaptureKeystrokes()) { + if (!this->keyboard_input.empty()) { + this->keyboard_input.pop_back(); + } + } +} + +void GUI::captureKeystroke(char c) { + if (this->shouldCaptureKeystrokes()) { + if (c >= 32 && c <= 126) { // meaningful character + this->keyboard_input.push_back(c); + } + } +} + +std::string GUI::getCapturedKeyboardInput() const { + return this->keyboard_input.c_str(); +} + +bool GUI::shouldCaptureKeystrokes() const { + return this->capture_keystrokes; +} + +void GUI::setCaptureKeystrokes(bool should_capture) { + this->capture_keystrokes = should_capture; +} + +void GUI::clearCapturedKeyboardInput() { + this->keyboard_input = ""; +} + + + +std::shared_ptr GUI::getFonts() { + return this->fonts; +} + +void GUI::layoutFrame(GUIState state) { + switch (state) { + case GUIState::TITLE_SCREEN: + glfwSetInputMode(client->window, GLFW_CURSOR, GLFW_CURSOR_NORMAL); + this->_layoutTitleScreen(); + break; + case GUIState::INITIAL_LOAD: + glfwSetInputMode(client->window, GLFW_CURSOR, GLFW_CURSOR_NORMAL); + this->_layoutLoadingScreen(); + break; + case GUIState::GAME_ESC_MENU: + glfwSetInputMode(client->window, GLFW_CURSOR, GLFW_CURSOR_NORMAL); + this->_layoutGameEscMenu(); + break; + case GUIState::LOBBY_BROWSER: + glfwSetInputMode(client->window, GLFW_CURSOR, GLFW_CURSOR_NORMAL); + this->_layoutLobbyBrowser(); + break; + case GUIState::GAME_HUD: + glfwSetInputMode(client->window, GLFW_CURSOR, GLFW_CURSOR_DISABLED); + this->_layoutGameHUD(); + break; + case GUIState::LOBBY: + glfwSetInputMode(client->window, GLFW_CURSOR, GLFW_CURSOR_NORMAL); + this->_layoutLobby(); + break; + case GUIState::NONE: + break; + } +} + +void GUI::_layoutLoadingScreen() { + this->addWidget(widget::CenterText::make( + "made by", + font::Font::MENU, + font::Size::MEDIUM, + font::Color::WHITE, + fonts, + FRAC_WINDOW_HEIGHT(7,8) + )); + this->addWidget(widget::CenterText::make( + "Torchlight Games", + font::Font::MENU, + font::Size::XLARGE, + font::Color::TORCHLIGHT_GAMES, + fonts, + FRAC_WINDOW_HEIGHT(2,3) + )); + this->addWidget(widget::CenterText::make( + "Loading...", + font::Font::MENU, + font::Size::MEDIUM, + font::Color::WHITE, + fonts, + FRAC_WINDOW_HEIGHT(1,3) + )); +} + +void GUI::_layoutTitleScreen() { + this->addWidget(widget::CenterText::make( + "Arcana", + font::Font::MENU, + font::Size::XLARGE, + font::Color::RED, + fonts, + FRAC_WINDOW_HEIGHT(2, 3) + )); + + auto start_text = widget::DynText::make( + "Start Game", + fonts, + widget::DynText::Options(font::Font::MENU, font::Size::MEDIUM, font::Color::BLACK) + ); + start_text->addOnHover([this](widget::Handle handle) { + auto widget = this->borrowWidget(handle); + widget->changeColor(font::Color::RED); + }); + start_text->addOnClick([this](widget::Handle handle) { + client->gui_state = GUIState::LOBBY_BROWSER; + }); + auto start_flex = widget::Flexbox::make( + glm::vec2(0.0f, FRAC_WINDOW_HEIGHT(1, 3)), + glm::vec2(WINDOW_WIDTH, 0.0f), + widget::Flexbox::Options(widget::Justify::VERTICAL, widget::Align::CENTER, 0.0f) + ); + + start_flex->push(std::move(start_text)); + this->addWidget(std::move(start_flex)); +} + +void GUI::_layoutLobbyBrowser() { + this->addWidget(widget::CenterText::make( + "Lobbies", + font::Font::MENU, + font::Size::LARGE, + font::Color::BLACK, + this->fonts, + WINDOW_HEIGHT - font::getFontSizePx(font::Size::LARGE) + )); + + auto lobbies_flex = widget::Flexbox::make( + glm::vec2(0.0f, FRAC_WINDOW_HEIGHT(1, 3)), + glm::vec2(WINDOW_WIDTH, 0.0f), + widget::Flexbox::Options(widget::Justify::VERTICAL, widget::Align::CENTER, 10.0f) + ); + + for (const auto& [ip, packet]: client->lobby_finder.getFoundLobbies()) { + std::stringstream ss; + ss << packet.lobby_name << " " << packet.slots_taken << "/" << packet.slots_avail + packet.slots_taken; + + auto entry = widget::DynText::make(ss.str(), this->fonts, + widget::DynText::Options(font::Font::MENU, font::Size::MEDIUM, font::Color::BLACK)); + entry->addOnClick([ip, this](widget::Handle handle){ + std::cout << "Connecting to " << ip.address() << " ...\n"; + this->client->connectAndListen(ip.address().to_string()); + this->client->gui_state = GUIState::LOBBY; + }); + entry->addOnHover([this](widget::Handle handle){ + auto widget = this->borrowWidget(handle); + widget->changeColor(font::Color::RED); + }); + lobbies_flex->push(std::move(entry)); + } + + if (client->lobby_finder.getFoundLobbies().empty()) { + lobbies_flex->push(widget::DynText::make( + "No lobbies found...", + this->fonts, + widget::DynText::Options(font::Font::MENU, font::Size::MEDIUM, font::Color::BLACK) + )); + } + + this->addWidget(std::move(lobbies_flex)); + + this->addWidget(widget::TextInput::make( + glm::vec2(FRAC_WINDOW_WIDTH(2, 5), FRAC_WINDOW_HEIGHT(1, 6)), + "Enter a name", + this, + fonts, + widget::DynText::Options(font::Font::TEXT, font::Size::MEDIUM, font::Color::BLACK) + )); +} + +void GUI::_layoutLobby() { + auto lobby_title = widget::CenterText::make( + this->client->gameState.lobby.name, + font::Font::MENU, + font::Size::LARGE, + font::Color::BLACK, + this->fonts, + WINDOW_HEIGHT - font::getFontSizePx(font::Size::LARGE) + ); + this->addWidget(std::move(lobby_title)); + std::stringstream ss; + ss << this->client->gameState.lobby.players.size() << " / " << this->client->gameState.lobby.max_players; + auto player_count = widget::CenterText::make( + ss.str(), + font::Font::MENU, + font::Size::MEDIUM, + font::Color::BLACK, + this->fonts, + WINDOW_HEIGHT - (2 * font::getFontSizePx(font::Size::LARGE)) - 10.0f + ); + this->addWidget(std::move(player_count)); + + auto players_flex = widget::Flexbox::make( + glm::vec2(0.0f, FRAC_WINDOW_HEIGHT(1, 5)), + glm::vec2(WINDOW_WIDTH, 0.0f), + widget::Flexbox::Options(widget::Justify::VERTICAL, widget::Align::CENTER, 10.0f) + ); + for (const auto& [_eid, player_name] : this->client->gameState.lobby.players) { + players_flex->push(widget::DynText::make( + player_name, + this->fonts, + widget::DynText::Options(font::Font::MENU, font::Size::MEDIUM, font::Color::BLACK) + )); + } + this->addWidget(std::move(players_flex)); + + auto waiting_msg = widget::CenterText::make( + "Waiting for players...", + font::Font::MENU, + font::Size::MEDIUM, + font::Color::GRAY, + this->fonts, + 30.0f + ); + this->addWidget(std::move(waiting_msg)); +} + +void GUI::_layoutGameHUD() { + +} + +void GUI::_layoutGameEscMenu() { + auto exit_game_txt = widget::DynText::make( + "Exit Game", + fonts, + widget::DynText::Options(font::Font::MENU, font::Size::MEDIUM, font::Color::BLACK) + ); + exit_game_txt->addOnHover([this](widget::Handle handle) { + auto widget = this->borrowWidget(handle); + widget->changeColor(font::Color::RED); + }); + exit_game_txt->addOnClick([this](widget::Handle handle) { + glfwSetWindowShouldClose(this->client->getWindow(), GL_TRUE); + }); + auto flex = widget::Flexbox::make( + glm::vec2(0.0f, FRAC_WINDOW_HEIGHT(1, 2)), + glm::vec2(WINDOW_WIDTH, 0.0f), + widget::Flexbox::Options(widget::Justify::VERTICAL, widget::Align::CENTER, 0.0f) + ); + flex->push(std::move(exit_game_txt)); + + this->addWidget(std::move(flex)); +} + +void GUI::_handleClick(float x, float y) { + for (const auto& [_, widget] : this->widgets) { + widget->doClick(x, y); + } +} + +void GUI::_handleHover(float x, float y) { + for (const auto& [_, widget] : this->widgets) { + widget->doHover(x, y); + } +} + +} diff --git a/src/client/gui/imgs/img.cpp b/src/client/gui/imgs/img.cpp new file mode 100644 index 00000000..4e12f286 --- /dev/null +++ b/src/client/gui/imgs/img.cpp @@ -0,0 +1,17 @@ +#include "client/gui/img/img.hpp" + +#include + +namespace gui::img { + +std::string getImgFilepath(ImgID img) { + auto img_root = getRepoRoot() / "assets/imgs"; + switch (img) { + default: + case ImgID::Yoshi: return (img_root / "Yoshi.png").string(); + case ImgID::Pikachu: return (img_root / "awesomeface.png").string(); + + } +} + +} diff --git a/src/client/gui/imgs/loader.cpp b/src/client/gui/imgs/loader.cpp new file mode 100644 index 00000000..364d9b2e --- /dev/null +++ b/src/client/gui/imgs/loader.cpp @@ -0,0 +1,73 @@ +#include "client/gui/img/loader.hpp" +#include "client/core.hpp" + +#include +#include + +#include "stb_image.h" + +namespace gui::img { + +bool Loader::init() { + std::cout << "Loading images...\n"; + + for (auto img_id : GET_ALL_IMG_IDS()) { + if (!this->_loadImg(img_id)) { // cppcheck-suppress useStlAlgorithm + return false; + } + } + + std::cout << "Loaded images\n"; + return true; +} + +bool Loader::_loadImg(ImgID img_id) { + GLuint texture_id; + + glGenTextures(1, &texture_id); + glBindTexture(GL_TEXTURE_2D, texture_id); + + // set Texture wrap and filter modes + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + + int width, height, channels; + + stbi_set_flip_vertically_on_load(true); + + auto path = getImgFilepath(img_id); + std::cout << "Loading " << path << "...\n"; + unsigned char* img_data = stbi_load(path.c_str(), &width, &height, &channels, 0); + + if (stbi_failure_reason()) + std::cout << "failure: " << stbi_failure_reason() << std::endl; + + if (img_data == 0 || width == 0 || height == 0) { + std::cerr << "Error loading " << path << std::endl; + return false; + } + + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, img_data); + glGenerateMipmap(GL_TEXTURE_2D); + + // unbind texture + glBindTexture(GL_TEXTURE_2D, 0); + + this->img_map.insert({img_id, Img { + .texture_id = texture_id, + .width = width, + .height = height + }}); + + stbi_image_free(img_data); + + return true; +} + +const Img& Loader::getImg(ImgID img_id) const { + return this->img_map.at(img_id); +} + +} diff --git a/src/client/gui/widget/centertext.cpp b/src/client/gui/widget/centertext.cpp new file mode 100644 index 00000000..e09ed7a5 --- /dev/null +++ b/src/client/gui/widget/centertext.cpp @@ -0,0 +1,25 @@ +#include "client/gui/widget/centertext.hpp" +#include "client/client.hpp" + +namespace gui::widget { + +Widget::Ptr CenterText::make( + std::string text, + font::Font font, + font::Size size, + font::Color color, + std::shared_ptr fonts, + float y_pos +) { + auto flex = widget::Flexbox::make( + glm::vec2(0.0f, y_pos), + glm::vec2(WINDOW_WIDTH, 0.0f), + widget::Flexbox::Options(widget::Justify::VERTICAL, widget::Align::CENTER, 0.0f) + ); + auto title = widget::DynText::make(text, fonts, + widget::DynText::Options(font, size, color)); + flex->push(std::move(title)); + return flex; +} + +} diff --git a/src/client/gui/widget/dyntext.cpp b/src/client/gui/widget/dyntext.cpp new file mode 100644 index 00000000..43e2dbc7 --- /dev/null +++ b/src/client/gui/widget/dyntext.cpp @@ -0,0 +1,110 @@ +#include "client/gui/widget/dyntext.hpp" +#include "client/gui/font/font.hpp" +#include "client/gui/font/loader.hpp" +#include "client/core.hpp" +#include "client/client.hpp" +#include "client/shader.hpp" + +#include +#include +#include +#include + +namespace gui::widget { + +std::unique_ptr DynText::shader = nullptr; + +DynText::DynText(glm::vec2 origin, const std::string& text, + std::shared_ptr fonts, DynText::Options options): + text(text), options(options), fonts(fonts), Widget(Type::DynText, origin) +{ + glGenVertexArrays(1, &VAO); + glGenBuffers(1, &VBO); + glBindVertexArray(VAO); + glBindBuffer(GL_ARRAY_BUFFER, VBO); + glBufferData(GL_ARRAY_BUFFER, sizeof(float) * 6 * 4, NULL, GL_DYNAMIC_DRAW); + glEnableVertexAttribArray(0); + glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 4 * sizeof(float), 0); + glBindBuffer(GL_ARRAY_BUFFER, 0); + glBindVertexArray(0); + + // Calculate size of string of text + this->width = 0; + this->height = 0; + + float scale = font::getScaleFactor(this->options.size); + + for (int i = 0; i < text.size(); i++) { + font::Character ch = this->fonts->loadChar(this->text[i], this->options.font); + this->height = std::max(this->height, static_cast(ch.size.y * scale)); + if (i != text.size() - 1 && i != 0) { + this->width += (ch.advance >> 6) * scale; + } else { + this->width += ch.size.x * scale; + } + } +} + +DynText::DynText(const std::string& text, std::shared_ptr fonts, DynText::Options options): + DynText({0.0f, 0.0f}, text, fonts, options) {} + +void DynText::render() { + glEnable(GL_CULL_FACE); + + DynText::shader->use(); + + auto projection = GUI_PROJECTION_MATRIX(); + DynText::shader->setMat4("projection", projection); + auto color = font::getRGB(this->options.color); + DynText::shader->setVec3("textColor", color); + glBindVertexArray(VAO); + + float x = this->origin.x; + float y = this->origin.y; + + // iterate through all characters + for (const char& c : this->text) + { + font::Character ch = this->fonts->loadChar(c, this->options.font); + + float scale = font::getScaleFactor(this->options.size); + + float xpos = x + ch.bearing.x * scale; + float ypos = y - (ch.size.y - ch.bearing.y) * scale; + + float w = ch.size.x * scale; + float h = ch.size.y * scale; + // update VBO for each character + float vertices[6][4] = { + { xpos, ypos + h, 0.0f, 0.0f }, + { xpos, ypos, 0.0f, 1.0f }, + { xpos + w, ypos, 1.0f, 1.0f }, + + { xpos, ypos + h, 0.0f, 0.0f }, + { xpos + w, ypos, 1.0f, 1.0f }, + { xpos + w, ypos + h, 1.0f, 0.0f } + }; + // render glyph texture over quad + glBindTexture(GL_TEXTURE_2D, ch.texture_id); + // update content of VBO memory + glBindBuffer(GL_ARRAY_BUFFER, VBO); + glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(vertices), vertices); + glBindBuffer(GL_ARRAY_BUFFER, 0); + // render quad + glDrawArrays(GL_TRIANGLES, 0, 6); + // now advance cursors for next glyph (note that advance is number of 1/64 pixels) + x += (ch.advance >> 6) * scale; // bitshift by 6 to get value in pixels (2^6 = 64) + } + + glBindVertexArray(0); + glBindTexture(GL_TEXTURE_2D, 0); + glUseProgram(0); + + glDisable(GL_CULL_FACE); +} + +void DynText::changeColor(font::Color new_color) { + this->options.color = new_color; +} + +} \ No newline at end of file diff --git a/src/client/gui/widget/flexbox.cpp b/src/client/gui/widget/flexbox.cpp new file mode 100644 index 00000000..f46fe532 --- /dev/null +++ b/src/client/gui/widget/flexbox.cpp @@ -0,0 +1,120 @@ +#include "client/gui/widget/flexbox.hpp" + +#include +#include +#include + +#include "client/core.hpp" + +namespace gui::widget { + +Flexbox::Flexbox(glm::vec2 origin, glm::vec2 size, Flexbox::Options options): + Widget(Type::Flexbox, origin), options(options) +{ + this->width = size.x; + this->height = size.y; +} + +Flexbox::Flexbox(glm::vec2 origin, Flexbox::Options options): + Flexbox(origin, {0.0f, 0.0f}, options) {} + +void Flexbox::doClick(float x, float y) { + Widget::doClick(x, y); + for (auto& widget : this->widgets) { + widget->doClick(x, y); + } +} + +void Flexbox::doHover(float x, float y) { + Widget::doHover(x, y); + for (auto& widget : this->widgets) { + widget->doHover(x, y); + } +} + +void Flexbox::push(Widget::Ptr&& widget) { + const auto& [new_width, new_height] = widget->getSize(); + + // Bless this mess! + + glm::vec2 prev_origin; + std::size_t prev_width; + std::size_t prev_height; + if (this->widgets.empty()) { + prev_origin = this->origin; + prev_width = this->width; + prev_height = this->height; + } else { + Widget::Ptr& prev_widget = this->widgets[this->widgets.size() - 1]; + prev_origin = prev_widget->getOrigin(); + prev_width = prev_widget->getSize().first; + prev_height = prev_widget->getSize().second; + } + + if (this->options.direction == Justify::HORIZONTAL) { + this->width += new_width + this->options.padding; + this->height = std::max(this->height, new_height); + glm::vec2 new_origin(prev_origin.x + prev_width + this->options.padding, prev_origin.y); + widget->setOrigin(new_origin); + } else if (this->options.direction == Justify::VERTICAL) { + this->height += new_height + this->options.padding; + this->width = std::max(this->width, new_width); + glm::vec2 new_origin(prev_origin.x, prev_origin.y + prev_height + this->options.padding); + widget->setOrigin(new_origin); + } + + this->widgets.push_back(std::move(widget)); + + if (this->options.alignment == Align::CENTER) { + if (this->options.direction == Justify::HORIZONTAL) { + std::cerr << "Note: center alignment not yet implemented for horizontal justify. Doing nothing\n"; + } else if (this->options.direction == Justify::VERTICAL) { + for (auto& widget : this->widgets) { + const auto [curr_width, _] = widget->getSize(); + glm::vec2 new_origin(this->origin.x + ((this->width - curr_width) / 2.0f), widget->getOrigin().y); + widget->setOrigin(new_origin); + } + } + } else if (this->options.alignment == Align::RIGHT) { + std::cerr << "Note: right alignment not yet implemented. Doing nothing\n"; + } // else it is align left, which is default behavior and requires no more messing + +} + +void Flexbox::render() { + // use x and y as origin coordinates, and render everything else based off of it + + for (const auto& widget : this->widgets) { + widget->render(); + } +} + +Widget* Flexbox::borrow(Handle handle) { + for (auto& widget : this->widgets) { + if (widget->getHandle() == handle) { + return widget.get(); + } + } + + if (handle != this->handle) { + std::cerr << "UI ERROR: Trying to borrow from Flexbox with invalid handle\n" + << "This should never happen, and this means that we are doing something\n" + << "very wrong." << std::endl; + std::exit(1); + } + + return this; +} + +bool Flexbox::hasHandle(Handle handle) const { + for (auto& widget : this->widgets) { + if (widget->getHandle() == handle) { + return true; + } + } + + return this->handle == handle; +} + +} + diff --git a/src/client/gui/widget/staticimg.cpp b/src/client/gui/widget/staticimg.cpp new file mode 100644 index 00000000..a1d363cc --- /dev/null +++ b/src/client/gui/widget/staticimg.cpp @@ -0,0 +1,79 @@ +#include "client/gui/gui.hpp" + +#include "client/client.hpp" +#include "client/shader.hpp" + + + +namespace gui::widget { + +std::unique_ptr StaticImg::shader = nullptr; + +StaticImg::StaticImg(glm::vec2 origin, gui::img::Img img): + Widget(Type::StaticImg, origin), img(img) +{ + float vertices[] = { + // positions // colors // texture coords + 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, // top right + 0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, // bottom right + -0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, // bottom left + -0.5f, 0.5f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f // top left + }; + unsigned int indices[] = { + 0, 1, 3, // first triangle + 1, 2, 3 // second triangle + }; + + glGenVertexArrays(1, &quadVAO); + glGenBuffers(1, &VBO); + glGenBuffers(1, &EBO); + + glBindVertexArray(quadVAO); + + glBindBuffer(GL_ARRAY_BUFFER, VBO); + glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); + + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); + glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW); + + // position attribute + glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)0); + glEnableVertexAttribArray(0); + // color attribute + glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(3 * sizeof(float))); + glEnableVertexAttribArray(1); + // texture coord attribute + glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float))); + glEnableVertexAttribArray(2); + + this->width = img.width; + this->height = img.height; + this->texture_id = img.texture_id; +} + +StaticImg::StaticImg(gui::img::Img img): + StaticImg({0.0f, 0.0f}, img) {} + +StaticImg::~StaticImg() { + glDeleteVertexArrays(1, &quadVAO); + glDeleteBuffers(1, &VBO); + glDeleteBuffers(1, &EBO); +} + +void StaticImg::render() { + StaticImg::shader->use(); + + glm::mat4 model = glm::mat4(1.0f); + + // glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, this->texture_id); + glBindVertexArray(quadVAO); + glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); + glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0); + + glBindVertexArray(0); + glBindTexture(GL_TEXTURE_2D, 0); + glUseProgram(0); +} + +} diff --git a/src/client/gui/widget/textinput.cpp b/src/client/gui/widget/textinput.cpp new file mode 100644 index 00000000..13894eb0 --- /dev/null +++ b/src/client/gui/widget/textinput.cpp @@ -0,0 +1,82 @@ +#include "client/gui/widget/textinput.hpp" +#include "client/core.hpp" +#include "client/gui/gui.hpp" +#include "client/client.hpp" +#include "shared/utilities/time.hpp" + +#include +#include + +namespace gui::widget { + +std::string TextInput::prev_input = ""; + +TextInput::TextInput(glm::vec2 origin, + const std::string& placeholder, + gui::GUI* gui, + std::shared_ptr fonts, + DynText::Options options): + Widget(Type::TextInput, origin), gui(gui) +{ + std::string text_to_display; + font::Color color_to_display; + std::string captured_input = gui->getCapturedKeyboardInput(); + if (captured_input.size() == 0) { + text_to_display = placeholder; + options.color = font::Color::GRAY; + } else { + text_to_display = captured_input; + } + + auto ms_since_epoch = getMsSinceEpoch(); + if (Client::getTimeOfLastKeystroke() + 1000 > ms_since_epoch || ms_since_epoch % 2000 > 1000) { + if (gui->shouldCaptureKeystrokes()) { + text_to_display += '|'; + } + } + + this->dyntext = DynText::make(origin, text_to_display, fonts, options); + this->dyntext->addOnClick([gui](widget::Handle handle) { + gui->setCaptureKeystrokes(true); + }); + + auto [width, height] = this->dyntext->getSize(); + this->width = width; + this->height = height; +} + +void TextInput::render() { + this->dyntext->render(); +} + +bool TextInput::hasHandle(Handle handle) const { + return handle == this->dyntext->getHandle() || + this->handle == handle; +} + +Widget* TextInput::borrow(Handle handle) { + if (this->handle == handle) { + return this; + } + + if (this->dyntext->getHandle() == handle) { + return this->dyntext.get(); + } + + std::cerr << "UI ERROR: Trying to borrow from Text Input with invalid handle\n" + << "This should never happen, and this means that we are doing something\n" + << "very wrong." << std::endl; + std::exit(1); +} + +void TextInput::doClick(float x, float y) { + Widget::doClick(x, y); + this->dyntext->doClick(x, y); +} + +void TextInput::doHover(float x, float y) { + Widget::doHover(x, y); + this->dyntext->doHover(x, y); +} + +} \ No newline at end of file diff --git a/src/client/gui/widget/widget.cpp b/src/client/gui/widget/widget.cpp new file mode 100644 index 00000000..41d66a81 --- /dev/null +++ b/src/client/gui/widget/widget.cpp @@ -0,0 +1,96 @@ +#include "client/gui/widget/widget.hpp" +#include "client/client.hpp" +#include "client/gui/gui.hpp" + +namespace gui::widget { + +std::size_t Widget::num_widgets = 0; + +Widget::Widget(Type type, glm::vec2 origin): + type(type), origin(origin) +{ + this->handle = num_widgets++; +} + +void Widget::setOrigin(glm::vec2 origin) { + this->origin = origin; +} + +const glm::vec2& Widget::getOrigin() const { + return this->origin; +} + +CallbackHandle Widget::addOnClick(Callback callback) { + CallbackHandle handle = this->next_click_handle++; + this->on_clicks.insert({handle, callback}); + return handle; +} + + +CallbackHandle Widget::addOnHover(Callback callback) { + CallbackHandle handle = this->next_hover_handle++; + this->on_hovers.insert({handle, callback}); + return handle; +} + +void Widget::removeOnClick(CallbackHandle handle) { + this->on_clicks.erase(handle); +} + +void Widget::removeOnHover(CallbackHandle handle) { + this->on_hovers.erase(handle); +} + +void Widget::doClick(float x, float y) { + if (this->_doesIntersect(x, y)) { + for (const auto& [_handle, callback] : this->on_clicks) { + callback(handle); + } + } +} + +void Widget::doHover(float x, float y) { + if (this->_doesIntersect(x, y)) { + for (const auto& [_handle, callback] : this->on_hovers) { + callback(handle); + } + } +} + +Type Widget::getType() const { + return this->type; +} + +std::pair Widget::getSize() const { + return {this->width, this->height}; +} + +bool Widget::_doesIntersect(float x, float y) const { + return ( + x > this->origin.x && + y > this->origin.y && + x < this->origin.x + this->width && + y < this->origin.y + this->height + ); +} + +Handle Widget::getHandle() const { + return this->handle; +} + +bool Widget::hasHandle(Handle handle) const { + return this->handle == handle; +} + +Widget* Widget::borrow(Handle handle) { + if (this->handle != handle) { + std::cerr << "UI ERROR: Trying to borrow from Widget with invalid handle\n" + << "This should never happen, and this means that we are doing something\n" + << "very wrong." << std::endl; + std::exit(1); + } + + return this; +} + +} diff --git a/src/client/lightsource.cpp b/src/client/lightsource.cpp new file mode 100644 index 00000000..363959d8 --- /dev/null +++ b/src/client/lightsource.cpp @@ -0,0 +1,37 @@ +#include "client/lightsource.hpp" + +#include + +#include + +#include "client/shader.hpp" + +LightSource::LightSource() : model(1.0f) { + glGenVertexArrays(1, &VAO); + glBindVertexArray(VAO); + // we only need to bind to the VBO, the container's VBO's data already contains the data. + glBindBuffer(GL_ARRAY_BUFFER, VBO); + // set the vertex attribute + glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); + glEnableVertexAttribArray(0); +} + +void LightSource::draw(std::shared_ptr shader, + glm::mat4 viewProj) { + shader->use(); + + // get the locations and send the uniforms to the shader + shader->setMat4("viewProj", viewProj); + shader->setMat4("model", model); + + // draw the light cube object + glBindVertexArray(VAO); + glDrawArrays(GL_TRIANGLES, 0, 36); + + glBindVertexArray(0); + glUseProgram(0); +} + +void LightSource::TranslateTo(const glm::vec3 &new_pos) { + model[3] = glm::vec4(new_pos, 1.0f); +} diff --git a/src/client/lobbyfinder.cpp b/src/client/lobbyfinder.cpp index 0207afc5..ce2e88c9 100644 --- a/src/client/lobbyfinder.cpp +++ b/src/client/lobbyfinder.cpp @@ -16,7 +16,6 @@ LobbyFinder::LobbyFinder(boost::asio::io_context& io_context, const GameConfig& keep_searching(false), lobby_info_buf() { - } LobbyFinder::~LobbyFinder() { diff --git a/src/client/main.cpp b/src/client/main.cpp index e495f0ac..9bd812ae 100644 --- a/src/client/main.cpp +++ b/src/client/main.cpp @@ -1,14 +1,19 @@ #include #include + #include #include #include "client/client.hpp" #include "shared/utilities/rng.hpp" #include "shared/utilities/config.hpp" +#include "shared/utilities/root_path.hpp" #include "client/sound.hpp" +#include "shared/utilities/root_path.hpp" +#include "client/gui/img/img.hpp" + using namespace std::chrono_literals; void error_callback(int error, const char* description) { @@ -16,19 +21,21 @@ void error_callback(int error, const char* description) { std::cerr << description << std::endl; } -void set_callbacks(GLFWwindow* window) { +void set_callbacks(GLFWwindow* window, Client* client) { // Set the error callback. glfwSetErrorCallback(error_callback); // Set the window resize callback. - // glfwSetWindowSizeCallback(window, client.resizeCallback); + // glfwSetWindowSizeCallback(window, Client::windowResizeCallback); // Set the key callback. glfwSetKeyCallback(window, Client::keyCallback); // Set the mouse and cursor callbacks - // glfwSetMouseButtonCallback(window, Client::mouseCallback); + glfwSetMouseButtonCallback(window, Client::mouseButtonCallback); glfwSetCursorPosCallback(window, Client::mouseCallback); + + glfwSetCharCallback(window, Client::charCallback); } void set_opengl_settings(GLFWwindow* window) { @@ -52,33 +59,24 @@ int main(int argc, char* argv[]) { auto config = GameConfig::parse(argc, argv); boost::asio::io_context context; - LobbyFinder lobby_finder(context, config); Client client(context, config); - if (config.client.lobby_discovery) { - // TODO: once we have UI, there should be a way to connect based on - // this. Right now, there isn't really a way to react to the information - // the LobbyFinder is gathering. - std::cerr << "Error: lobby discovery not enabled yet for client-side." - << std::endl; - std::exit(1); - // lobby_finder.startSearching(); - } else { - client.connectAndListen(config.network.server_ip); - } if (!client.init()) { + std::cout << "client init failed" << std::endl; exit(EXIT_FAILURE); } GLFWwindow* window = client.getWindow(); if (!window) exit(EXIT_FAILURE); + glfwSetWindowUserPointer(window, &client); + // Setup callbacks. - set_callbacks(window); + set_callbacks(window, &client); // Setup OpenGL settings. set_opengl_settings(window); - boost::filesystem::path soundFilepath = client.getRootPath() / "assets/sounds/piano.wav"; + boost::filesystem::path soundFilepath = getRepoRoot() / "assets/sounds/piano.wav"; sf::SoundBuffer buffer; if (!buffer.loadFromFile(soundFilepath.string())) diff --git a/src/client/model.cpp b/src/client/model.cpp new file mode 100644 index 00000000..38f7200c --- /dev/null +++ b/src/client/model.cpp @@ -0,0 +1,343 @@ +#include "client/model.hpp" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "assimp/material.h" +#include "assimp/types.h" +#include "client/util.hpp" +#include "glm/ext/matrix_transform.hpp" +#include +#include +#include + +#define STB_IMAGE_IMPLEMENTATION +#include + +#define GLM_ENABLE_EXPERIMENTAL +#include "glm/ext/matrix_clip_space.hpp" +#include "glm/fwd.hpp" +#include +#include +#include +#include + + +Mesh::Mesh( + const std::vector& vertices, + const std::vector& indices, + const std::vector& textures, + const Material& material) : + vertices(vertices), indices(indices), textures(textures), material(material) { + + glGenVertexArrays(1, &VAO); + glGenBuffers(1, &VBO); + glGenBuffers(1, &EBO); + + glBindVertexArray(VAO); + glBindBuffer(GL_ARRAY_BUFFER, VBO); + glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(Vertex), &vertices[0], GL_STATIC_DRAW); + + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); + glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.size() * sizeof(unsigned int), &indices[0], GL_STATIC_DRAW); + + // vertex positions + glEnableVertexAttribArray(0); + glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), reinterpret_cast(0)); + + // vertex normals + glEnableVertexAttribArray(1); + glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), reinterpret_cast(offsetof(Vertex, normal))); + + // vertex texture coords + glEnableVertexAttribArray(2); + glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), reinterpret_cast(offsetof(Vertex, textureCoords))); + + glBindBuffer(GL_ARRAY_BUFFER, 0); + glBindVertexArray(0); + + std::cout << "Loaded mesh with " << vertices.size() << " vertices, and " << textures.size() << " textures" << std::endl; + std::cout << "\t diffuse " << glm::to_string(this->material.diffuse) << std::endl; + std::cout << "\t ambient " << glm::to_string(this->material.diffuse) << std::endl; + std::cout << "\t specular " << glm::to_string(this->material.specular) << std::endl; + std::cout << "\t shininess" << this->material.shininess << std::endl; +} + +void Mesh::draw( + std::shared_ptr shader, + glm::mat4 viewProj, + glm::vec3 camPos, + glm::vec3 lightPos, + bool fill) { + // actiavte the shader program + shader->use(); + + // vertex shader uniforms + shader->setMat4("viewProj", viewProj); + auto model = this->getModelMat(); + shader->setMat4("model", model); + + // fragment shader uniforms + shader->setVec3("material.diffuse", this->material.diffuse); + shader->setVec3("material.ambient", this->material.ambient); + shader->setVec3("material.specular", this->material.specular); + shader->setFloat("material.shininess", this->material.shininess); + shader->setVec3("viewPos", camPos); + auto lightColor = glm::vec3(1.0f, 1.0f, 1.0f); + shader->setVec3("lightColor", lightColor); + shader->setVec3("lightPos", lightPos); + + if (textures.size() == 0) { + } else { + unsigned int diffuseNr = 1; + unsigned int specularNr = 1; + for(unsigned int i = 0; i < textures.size(); i++) { + glActiveTexture(GL_TEXTURE0 + i); // activate proper texture unit before binding + // retrieve texture number (the N in diffuse_textureN) + std::string number; + std::string name = textures[i].getType(); + if(name == "texture_diffuse") + number = std::to_string(diffuseNr++); + else if(name == "texture_specular") + number = std::to_string(specularNr++); + + std::string shaderTextureName = "material." + name + number; + shader->setInt(shaderTextureName, i); + glBindTexture(GL_TEXTURE_2D, textures[i].getID()); + } + glActiveTexture(GL_TEXTURE0); + } + + // draw mesh + glBindVertexArray(VAO); + if(fill){ + glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); + } else { + glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); + } + glDrawElements(GL_TRIANGLES, indices.size(), GL_UNSIGNED_INT, 0); + glBindVertexArray(0); + glUseProgram(0); +} + +Model::Model(const std::string& filepath) { + this->directory = std::filesystem::path(filepath).parent_path().string(); + + Assimp::Importer importer; + const aiScene *scene = importer.ReadFile(filepath, aiProcess_Triangulate | aiProcess_FlipUVs | aiProcess_SplitLargeMeshes | aiProcess_OptimizeMeshes); + if(!scene || scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE || !scene->mRootNode) { + throw std::invalid_argument(std::string("ERROR::ASSIMP::") + importer.GetErrorString()); + } + + processNode(scene->mRootNode, scene); +} + +void Model::draw(std::shared_ptr shader, + glm::mat4 viewProj, + glm::vec3 camPos, + glm::vec3 lightPos, + bool fill) { + + for(Mesh& mesh : this->meshes) { + mesh.draw(shader, viewProj, camPos, lightPos, fill); + } +} + +void Model::translateAbsolute(const glm::vec3& new_pos) { + for(Mesh& mesh : this->meshes) { + mesh.translateAbsolute(new_pos); + } +} + +void Model::translateRelative(const glm::vec3& delta) { + for(Mesh& mesh : this->meshes) { + mesh.translateAbsolute(delta); + } +} + +void Model::scale(const float& new_factor) { + for(Mesh& mesh : this->meshes) { + mesh.scale(new_factor); + } +} + +void Model::scale(const glm::vec3& scale) { + for(Mesh& mesh : this->meshes) { + mesh.scale(scale); + } +} + +void Model::processNode(aiNode *node, const aiScene *scene) { + // process all the node's meshes (if any) + for(unsigned int i = 0; i < node->mNumMeshes; i++) { + aiMesh *mesh = scene->mMeshes[node->mMeshes[i]]; + meshes.push_back(processMesh(mesh, scene)); + } + // then do the same for each of its children + for(unsigned int i = 0; i < node->mNumChildren; i++) { + processNode(node->mChildren[i], scene); + } +} + +Mesh Model::processMesh(aiMesh *mesh, const aiScene *scene) { + std::vector vertices; + std::vector indices; + std::vector textures; + + // process vertex positions, normals and texture coordinates + for(unsigned int i = 0; i < mesh->mNumVertices; i++) { + glm::vec3 position( + mesh->mVertices[i].x, + mesh->mVertices[i].y, + mesh->mVertices[i].z); + glm::vec3 normal( + mesh->mNormals[i].x, + mesh->mNormals[i].y, + mesh->mNormals[i].z); + + // check if the mesh contain texture coordinates + glm::vec2 texture(0.0f, 0.0f); + if(mesh->mTextureCoords[0]) { + texture.x = mesh->mTextureCoords[0][i].x; + texture.y = mesh->mTextureCoords[0][i].y; + } + + vertices.push_back(Vertex{ + position, + normal, + texture + }); + } + // process indices + for(unsigned int i = 0; i < mesh->mNumFaces; i++) { + aiFace face = mesh->mFaces[i]; + for(unsigned int j = 0; j < face.mNumIndices; j++) + indices.push_back(face.mIndices[j]); + } + + // process material + aiColor3D diffuse_color; + aiColor3D ambient_color; + aiColor3D specular_color; + float shininess = 0.0f; + + if(mesh->mMaterialIndex >= 0) { + std::cout << "processing material of id: " << mesh->mMaterialIndex << std::endl; + aiMaterial* material = scene->mMaterials[mesh->mMaterialIndex]; + + std::vector diffuseMaps = loadMaterialTextures(material, aiTextureType_DIFFUSE); + textures.insert(textures.end(), diffuseMaps.begin(), diffuseMaps.end()); + + std::vector specularMaps = loadMaterialTextures(material, aiTextureType_SPECULAR); + textures.insert(textures.end(), specularMaps.begin(), specularMaps.end()); + + if(AI_SUCCESS != material->Get(AI_MATKEY_COLOR_DIFFUSE, diffuse_color)) { + std::cout << "couldn't get diffuse color" << std::endl; + } + + if(AI_SUCCESS != material->Get(AI_MATKEY_COLOR_AMBIENT, ambient_color)) { + std::cout << "couldn't get ambient color" << std::endl; + } + + if(AI_SUCCESS != material->Get(AI_MATKEY_COLOR_SPECULAR, specular_color)) { + std::cout << "couldn't get specular color" << std::endl; + } + + if(AI_SUCCESS != material->Get(AI_MATKEY_SHININESS, shininess)) { + std::cout << "couldn't get shininess factor" << std::endl; + } + } + + return Mesh( + vertices, + indices, + textures, + Material { + aiColorToGLM(diffuse_color), + aiColorToGLM(ambient_color), + aiColorToGLM(specular_color), + shininess + } + ); +} + + +std::vector Model::loadMaterialTextures(aiMaterial* mat, const aiTextureType& type) { + std::vector textures; + std::cout << "material has " << mat->GetTextureCount(type) << " textures of type " << aiTextureTypeToString(type) << std::endl; + for(unsigned int i = 0; i < mat->GetTextureCount(type); i++) { + aiString str; + mat->GetTexture(type, i, &str); + std::filesystem::path textureFilepath = std::filesystem::path(this->directory) / std::string(str.C_Str()); + Texture texture(textureFilepath.string(), type); + textures.push_back(texture); + } + return textures; +} + + +Texture::Texture(const std::string& filepath, const aiTextureType& type) { + switch (type) { + case aiTextureType_DIFFUSE: + this->type = "texture_diffuse"; + break; + case aiTextureType_SPECULAR: + this->type = "texture_specular"; + break; + default: + throw std::invalid_argument(std::string("Unimplemented texture type ") + aiTextureTypeToString(type)); + } + + unsigned int textureID; + glGenTextures(1, &textureID); + + int width, height, nrComponents; + std::cout << "Attempting to load texture at " << filepath << std::endl; + unsigned char *data = stbi_load(filepath.c_str(), &width, &height, &nrComponents, 0); + if (!data) { + std::cout << "Texture failed to load at path: " << filepath << std::endl; + stbi_image_free(data); + throw std::exception(); + } + std::cout << "Succesfully loaded texture at " << filepath << std::endl; + GLenum format = GL_RED; + if (nrComponents == 1) + format = GL_RED; + else if (nrComponents == 3) + format = GL_RGB; + else if (nrComponents == 4) + format = GL_RGBA; + + glBindTexture(GL_TEXTURE_2D, textureID); + glTexImage2D(GL_TEXTURE_2D, 0, format, width, height, 0, format, GL_UNSIGNED_BYTE, data); + glGenerateMipmap(GL_TEXTURE_2D); + + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + + stbi_image_free(data); + + this->ID = textureID; +} + +unsigned int Texture::getID() const { + return ID; +} + +std::string Texture::getType() const { + return type; +} + + diff --git a/src/client/renderable.cpp b/src/client/renderable.cpp new file mode 100644 index 00000000..5094337c --- /dev/null +++ b/src/client/renderable.cpp @@ -0,0 +1,28 @@ +#include "client/renderable.hpp" + +#include +#define GLM_ENABLE_EXPERIMENTAL +#include + +Renderable::Renderable() : model(1.0f) { } + +void Renderable::translateAbsolute(const glm::vec3 &new_pos) { + this->model[3] = glm::vec4(new_pos, 1.0f); +} + +void Renderable::translateRelative(const glm::vec3& delta) { + this->model = glm::translate(this->model, delta); +} + +void Renderable::scale(const float& new_factor) { + glm::vec3 scaleVector(new_factor, new_factor, new_factor); + this->model = glm::scale(this->model, scaleVector); +} + +void Renderable::scale(const glm::vec3& scale) { + this->model = glm::scale(this->model, scale); +} + +glm::mat4 Renderable::getModelMat() { + return this->model; +} diff --git a/src/client/shader.cpp b/src/client/shader.cpp new file mode 100644 index 00000000..437c115c --- /dev/null +++ b/src/client/shader.cpp @@ -0,0 +1,127 @@ +#include + +#include +#include +#include + +#include +#include + +#include "client/shader.hpp" + +Shader::Shader(const std::string& vertexPath, const std::string& fragmentPath) { + std::string vertexCode; + std::string fragmentCode; + std::ifstream vShaderFile; + std::ifstream fShaderFile; + + // ensure ifstream objects can throw exceptions + vShaderFile.exceptions (std::ifstream::failbit | std::ifstream::badbit); + fShaderFile.exceptions (std::ifstream::failbit | std::ifstream::badbit); + try { + vShaderFile.open(vertexPath); + fShaderFile.open(fragmentPath); + + std::stringstream vShaderStream, fShaderStream; + + vShaderStream << vShaderFile.rdbuf(); + fShaderStream << fShaderFile.rdbuf(); + + vShaderFile.close(); + fShaderFile.close(); + + vertexCode = vShaderStream.str(); + fragmentCode = fShaderStream.str(); + } catch(std::ifstream::failure& e) { + throw std::invalid_argument("Error: could not read shader file " + vertexPath + " and " + fragmentPath); + } + + const char* vShaderCode = vertexCode.c_str(); + const char* fShaderCode = fragmentCode.c_str(); + + GLuint vertex, fragment; + int success; + char infoLog[512]; + + // vertex Shader + vertex = glCreateShader(GL_VERTEX_SHADER); + glShaderSource(vertex, 1, &vShaderCode, NULL); + glCompileShader(vertex); + // print compile errors if any + glGetShaderiv(vertex, GL_COMPILE_STATUS, &success); + if(!success) { + glGetShaderInfoLog(vertex, 512, NULL, infoLog); + std::cout << "ERROR: Vertex shader compilation failed\n" << infoLog << std::endl; + }; + + // fragment shader + fragment = glCreateShader(GL_FRAGMENT_SHADER); + glShaderSource(fragment, 1, &fShaderCode, NULL); + glCompileShader(fragment); + // print compile errors if any + glGetShaderiv(fragment, GL_COMPILE_STATUS, &success); + if(!success) { + glGetShaderInfoLog(vertex, 512, NULL, infoLog); + std::cout << "ERROR: Fragment shader compilation failed\n" << infoLog << std::endl; + }; + + if (vertex == 0 && fragment == 0) { + throw new std::invalid_argument("both shaders failed to init"); + } + + // shader Program + ID = glCreateProgram(); + glAttachShader(ID, vertex); + glAttachShader(ID, fragment); + glLinkProgram(ID); + // print linking errors if any + glGetProgramiv(ID, GL_LINK_STATUS, &success); + if(!success) { + glGetProgramInfoLog(ID, 512, NULL, infoLog); + std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog << std::endl; + } + + // delete the shaders as they're linked into our program now and no longer necessary + glDetachShader(ID, vertex); + glDetachShader(ID, fragment); + glDeleteShader(vertex); + glDeleteShader(fragment); +} + +Shader::~Shader() { + glDeleteProgram(this->ID); +} + +void Shader::use() { + glUseProgram(ID); +} + +void Shader::setBool(const std::string &name, bool value) const { + glUniform1i(glGetUniformLocation(ID, name.c_str()), (int)value); +} + +void Shader::setInt(const std::string &name, int value) const { + glUniform1i(glGetUniformLocation(ID, name.c_str()), value); +} + +void Shader::setFloat(const std::string &name, float value) const { + glUniform1f(glGetUniformLocation(ID, name.c_str()), value); +} + +void Shader::setMat4(const std::string &name, glm::mat4& value) { + glUniformMatrix4fv(glGetUniformLocation(ID, name.c_str()), 1, GL_FALSE, reinterpret_cast(&value)); +} + +void Shader::setVec3(const std::string &name, glm::vec3& value) { + glUniform3fv(glGetUniformLocation(ID, name.c_str()), 1, reinterpret_cast(&value)); +} + +glm::vec3 Shader::getVec3(const std::string &name) { + glm::vec3 vec; + glGetUniformfv(ID, glGetUniformLocation(ID, name.c_str()), reinterpret_cast(&vec)); + return vec; +} + +GLuint Shader::getID() { + return ID; +} diff --git a/src/client/shaders.cpp b/src/client/shaders.cpp deleted file mode 100644 index 2a7ea5f0..00000000 --- a/src/client/shaders.cpp +++ /dev/null @@ -1,19 +0,0 @@ -#include -#include - -#include - -#include "client/util.hpp" - -GLuint loadCubeShaders() { - boost::filesystem::path root_path = boost::dll::program_location().parent_path().parent_path().parent_path(); - boost::filesystem::path vertFilepath = root_path / "src/client/shaders/shader.vert"; - boost::filesystem::path fragFilepath = root_path / "src/client/shaders/shader.frag"; - - GLuint shaderProgram = LoadShaders(vertFilepath.string(), fragFilepath.string()); - // Check the shader program exists and is non-zero - if (!shaderProgram) { - return 0; - } - return shaderProgram; -} diff --git a/src/client/shaders/shader.frag b/src/client/shaders/cube.frag similarity index 62% rename from src/client/shaders/shader.frag rename to src/client/shaders/cube.frag index 9b84c61c..9c30ceae 100644 --- a/src/client/shaders/shader.frag +++ b/src/client/shaders/cube.frag @@ -1,22 +1,18 @@ #version 330 core -// Inputs to the fragment shader are the outputs of the same name from the vertex shader. -// Note that you do not have access to the vertex shader's default output, gl_Position. +// Fragment shader of solid color, untextured cubes in vec3 fragNormal; +in vec3 fragPos; -// uniforms used for lighting uniform vec3 AmbientColor = vec3(0.2); uniform vec3 LightDirection = normalize(vec3(2, 4, 3)); uniform vec3 LightColor = vec3(1.0, 1.0, 1.0); uniform vec3 DiffuseColor = vec3(1.0, 1.0, 1.0); -// You can output many things. The first vec4 type output determines the color of the fragment out vec4 fragColor; -void main() -{ - +void main() { // Compute irradiance (sum of ambient & direct lighting) vec3 irradiance = AmbientColor + LightColor * max(0, dot(LightDirection, fragNormal)); diff --git a/src/client/shaders/shader.vert b/src/client/shaders/cube.vert similarity index 79% rename from src/client/shaders/shader.vert rename to src/client/shaders/cube.vert index 56567efe..2ae4fdda 100644 --- a/src/client/shaders/shader.vert +++ b/src/client/shaders/cube.vert @@ -1,5 +1,6 @@ #version 330 core -// NOTE: Do NOT use any version older than 330! Bad things will happen! +# +// Fragment shader of solid color, untextured cubes layout (location = 0) in vec3 position; layout (location = 1) in vec3 normal; @@ -11,7 +12,7 @@ uniform mat4 model; // Outputs of the vertex shader are the inputs of the same name of the fragment shader. // The default output, gl_Position, should be assigned something. out vec3 fragNormal; - +out vec3 fragPos; void main() { @@ -20,4 +21,6 @@ void main() // for shading fragNormal = vec3(model * vec4(normal, 0)); + //fragNormal = normal; + fragPos = vec3(model * vec4(position, 1.0)); } \ No newline at end of file diff --git a/src/client/shaders/img.frag b/src/client/shaders/img.frag new file mode 100644 index 00000000..d7bb3647 --- /dev/null +++ b/src/client/shaders/img.frag @@ -0,0 +1,13 @@ +#version 330 core +out vec4 FragColor; + +in vec3 ourColor; +in vec2 TexCoord; + +// texture sampler +uniform sampler2D texture1; + +void main() +{ + FragColor = texture(texture1, TexCoord); +} \ No newline at end of file diff --git a/src/client/shaders/img.vert b/src/client/shaders/img.vert new file mode 100644 index 00000000..4fd66038 --- /dev/null +++ b/src/client/shaders/img.vert @@ -0,0 +1,14 @@ +#version 330 core +layout (location = 0) in vec3 aPos; +layout (location = 1) in vec3 aColor; +layout (location = 2) in vec2 aTexCoord; + +out vec3 ourColor; +out vec2 TexCoord; + +void main() +{ + gl_Position = vec4(aPos, 1.0); + ourColor = aColor; + TexCoord = vec2(aTexCoord.x, aTexCoord.y); +} \ No newline at end of file diff --git a/src/client/shaders/lightsource.frag b/src/client/shaders/lightsource.frag new file mode 100644 index 00000000..6b1b3513 --- /dev/null +++ b/src/client/shaders/lightsource.frag @@ -0,0 +1,7 @@ +#version 330 core +out vec4 FragColor; + +void main() +{ + FragColor = vec4(1.0); // set all 4 vector values to 1.0 +} diff --git a/src/client/shaders/lightsource.vert b/src/client/shaders/lightsource.vert new file mode 100644 index 00000000..820e2b27 --- /dev/null +++ b/src/client/shaders/lightsource.vert @@ -0,0 +1,14 @@ +#version 330 core +// NOTE: Do NOT use any version older than 330! Bad things will happen! + +layout (location = 0) in vec3 position; + +// Uniform variables +uniform mat4 viewProj; +uniform mat4 model; + +void main() +{ + // OpenGL maintains the D matrix so you only need to multiply by P, V (aka C inverse), and M + gl_Position = viewProj * model * vec4(position, 1.0); +} diff --git a/src/client/shaders/model.frag b/src/client/shaders/model.frag new file mode 100644 index 00000000..e08d85d3 --- /dev/null +++ b/src/client/shaders/model.frag @@ -0,0 +1,47 @@ +#version 330 core + +// Fragment shader for loaded models. +// This shader currently expects textured models. Untextured +// models will show up as black. + +in vec3 fragNormal; +in vec3 fragPos; +in vec2 TexCoords; + +struct Material { + vec3 ambient; + vec3 diffuse; + vec3 specular; + float shininess; + sampler2D texture_diffuse1; +}; + +uniform Material material; +uniform vec3 viewPos; + +uniform vec3 lightPos; +uniform vec3 lightColor; + +out vec4 fragColor; + +void main() { + // ambient + vec3 ambient = lightColor * vec3(texture(material.texture_diffuse1, TexCoords)); + + // diffuse + vec3 norm = normalize(fragNormal); + vec3 lightDir = normalize(lightPos - fragPos); + float diff = max(dot(norm, lightDir), 0.0); + vec3 diffuse = lightColor * (diff * vec3(texture(material.texture_diffuse1, TexCoords))); + + // specular + vec3 viewDir = normalize(viewPos - fragPos); + vec3 reflectDir = reflect(-lightDir, norm); + float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess); + vec3 specular = lightColor * (spec * material.specular); + + vec3 result = ambient + diffuse + specular; + fragColor = vec4(result, 1.0); + // * vec4(texture(material.texture_diffuse1, TexCoords)); + // fragColor = vec4(0.0, 1.0, 1.0, 1.0); +} diff --git a/src/client/shaders/model.vert b/src/client/shaders/model.vert new file mode 100644 index 00000000..7d681852 --- /dev/null +++ b/src/client/shaders/model.vert @@ -0,0 +1,29 @@ +#version 330 core +# +// Vertex shader for loaded models. +// Also forwards texture coordinates to fragment shader. + +layout (location = 0) in vec3 position; +layout (location = 1) in vec3 normal; +layout (location = 2) in vec2 uvs; + +// Uniform variables +uniform mat4 viewProj; +uniform mat4 model; + +// Outputs of the vertex shader are the inputs of the same name of the fragment shader. +// The default output, gl_Position, should be assigned something. +out vec3 fragNormal; +out vec3 fragPos; +out vec2 TexCoords; + +void main() { + // OpenGL maintains the D matrix so you only need to multiply by P, V (aka C inverse), and M + gl_Position = viewProj * model * vec4(position, 1.0); + + // for shading + fragNormal = vec3(model * vec4(normal, 0)); + //fragNormal = normal; + fragPos = vec3(model * vec4(position, 1.0)); + TexCoords = uvs; +} diff --git a/src/client/shaders/text.frag b/src/client/shaders/text.frag new file mode 100644 index 00000000..e4ea6554 --- /dev/null +++ b/src/client/shaders/text.frag @@ -0,0 +1,15 @@ +#version 330 core +in vec2 TexCoords; +out vec4 color; + +uniform sampler2D text; +uniform vec3 textColor; + +// Fragment shader for rendering text, copied from +// https://learnopengl.com/In-Practice/Text-Rendering + +void main() +{ + vec4 sampled = vec4(1.0, 1.0, 1.0, texture(text, TexCoords).r); + color = vec4(textColor, 1.0) * sampled; +} diff --git a/src/client/shaders/text.vert b/src/client/shaders/text.vert new file mode 100644 index 00000000..e3360d36 --- /dev/null +++ b/src/client/shaders/text.vert @@ -0,0 +1,14 @@ +#version 330 core +layout (location = 0) in vec4 vertex; // +out vec2 TexCoords; + +uniform mat4 projection; + +// Vertex shader for rendering text, copied from +// https://learnopengl.com/In-Practice/Text-Rendering + +void main() +{ + gl_Position = projection * vec4(vertex.xy, 0.0, 1.0); + TexCoords = vertex.zw; +} diff --git a/src/client/tests/CMakeLists.txt b/src/client/tests/CMakeLists.txt index fbe4eb69..8f770390 100644 --- a/src/client/tests/CMakeLists.txt +++ b/src/client/tests/CMakeLists.txt @@ -5,13 +5,25 @@ set(FILES ) add_executable(${TARGET_NAME} ${FILES}) -target_link_libraries(${TARGET_NAME} PRIVATE game_client_lib game_shared_lib) +target_link_libraries(${TARGET_NAME} PRIVATE game_client_lib game_shared_lib game_gui_lib) target_include_directories(${TARGET_NAME} PRIVATE ${INCLUDE_DIRECTORY}) target_link_libraries(${TARGET_NAME} PUBLIC gtest_main) add_test(NAME ${TARGET_NAME} COMMAND ${TARGET_NAME}) target_include_directories(${TARGET_NAME} PRIVATE ${BOOST_LIBRARY_INCLUDES}) +target_include_directories(${TARGET_NAME} + PRIVATE + ${OPENGL_INCLUDE_DIRS} + glfw + glm + ${imgui-directory} + ${GLEW_INCLUDE_DIRS} + ${ASSIMP_INCLUDE_DIRS} + ${STB_INCLUDE_DIRS} + ${FREETYPE_INCLUDE_DIRS} +) + target_link_libraries(${TARGET_NAME} PRIVATE Boost::asio @@ -20,9 +32,11 @@ target_link_libraries(${TARGET_NAME} Boost::program_options Boost::serialization nlohmann_json::nlohmann_json + glm + glfw + libglew_static + freetype ) -target_include_directories(${TARGET_NAME} PRIVATE ${OPENGL_INCLUDE_DIRS} glfw glm ${imgui-directory} "${CMAKE_BINARY_DIR}/_deps/glew-src/include") -target_link_libraries(${TARGET_NAME} PRIVATE glm glfw libglew_static) # setup make target set(RUN_TESTS_TARGET "run_${TARGET_NAME}") diff --git a/src/client/util.cpp b/src/client/util.cpp index b5357dd3..39a0ffac 100644 --- a/src/client/util.cpp +++ b/src/client/util.cpp @@ -1,79 +1,6 @@ #include "client/util.hpp" -enum ShaderType { - vertex, - fragment -}; - -GLuint LoadSingleShader(const std::string& shaderFilePath, ShaderType type) { - // Create a shader id. - GLuint shaderID = 0; - - if (type == vertex) - shaderID = glCreateShader(GL_VERTEX_SHADER); - else if (type == fragment) - shaderID = glCreateShader(GL_FRAGMENT_SHADER); - - // Try to read shader codes from the shader file. - std::string shaderCode; - std::ifstream shaderStream(shaderFilePath, std::ios::in); - if (shaderStream.is_open()) { - std::string Line = ""; - while (getline(shaderStream, Line)) - shaderCode += "\n" + Line; - shaderStream.close(); - } else { - std::cerr << "Impossible to open " << shaderFilePath << ". " - << "Check to make sure the file exists and you passed in the " - << "right filepath!" - << std::endl; - return 0; - } - - GLint Result = GL_FALSE; - - // Compile Shader. - std::cerr << "Compiling shader: " << shaderFilePath << std::endl; - char const* sourcePointer = shaderCode.c_str(); - glShaderSource(shaderID, 1, &sourcePointer, NULL); - glCompileShader(shaderID); - - // Check Shader. - glGetShaderiv(shaderID, GL_COMPILE_STATUS, &Result); - if (type == vertex) - printf("Successfully compiled vertex shader!\n"); - else if (type == fragment) - printf("Successfully compiled fragment shader!\n"); - - return shaderID; +glm::vec3 aiColorToGLM(const aiColor3D& color) { + return glm::vec3(color.r, color.g, color.b); } -GLuint LoadShaders(const std::string& vertexFilePath, const std::string& fragmentFilePath) { - // Create the vertex shader and fragment shader. - GLuint vertexShaderID = LoadSingleShader(vertexFilePath, vertex); - GLuint fragmentShaderID = LoadSingleShader(fragmentFilePath, fragment); - - // Check both shaders. - if (vertexShaderID == 0 || fragmentShaderID == 0) return 0; - - GLint Result = GL_FALSE; - - // Link the program. - printf("Linking program\n"); - GLuint programID = glCreateProgram(); - glAttachShader(programID, vertexShaderID); - glAttachShader(programID, fragmentShaderID); - glLinkProgram(programID); - - // Check the program. - glGetProgramiv(programID, GL_LINK_STATUS, &Result); - printf("Successfully linked program!\n"); - - // Detach and delete the shaders as they are no longer needed. - glDetachShader(programID, vertexShaderID); - glDetachShader(programID, fragmentShaderID); - glDeleteShader(vertexShaderID); - glDeleteShader(fragmentShaderID); - - return programID; -} diff --git a/src/server/game/enemy.cpp b/src/server/game/enemy.cpp index d1c331a6..96befe58 100644 --- a/src/server/game/enemy.cpp +++ b/src/server/game/enemy.cpp @@ -6,6 +6,7 @@ SharedObject Enemy::toShared() { return so; } -Enemy::Enemy() : Creature(ObjectType::Enemy) {} +Enemy::Enemy() : Creature(ObjectType::Enemy) { +} Enemy::~Enemy() {} \ No newline at end of file diff --git a/src/server/game/gridcell.cpp b/src/server/game/gridcell.cpp index cb0e3f7c..c73ac81d 100644 --- a/src/server/game/gridcell.cpp +++ b/src/server/game/gridcell.cpp @@ -11,6 +11,8 @@ CellType charToCellType(char c) { return CellType::Wall; case '@': return CellType::Spawn; + case 'E': + return CellType::Enemy; default: return CellType::Unknown; } diff --git a/src/server/game/item.cpp b/src/server/game/item.cpp index dbb256bf..e655869d 100644 --- a/src/server/game/item.cpp +++ b/src/server/game/item.cpp @@ -2,7 +2,7 @@ #include "shared/game/sharedobject.hpp" /* Constructors and Destructors */ -Item::Item() : Object(ObjectType::Item) { +Item::Item() : Object(ObjectType::Item) { // cppcheck-suppress uninitMemberVar } diff --git a/src/server/game/objectmanager.cpp b/src/server/game/objectmanager.cpp index 49e4ceae..330d0bcb 100644 --- a/src/server/game/objectmanager.cpp +++ b/src/server/game/objectmanager.cpp @@ -1,4 +1,5 @@ #include "server/game/objectmanager.hpp" +#include "server/game/enemy.hpp" #include @@ -70,8 +71,22 @@ SpecificID ObjectManager::createObject(ObjectType type) { player->globalID = globalID; break; } - case ObjectType::Object: - default: { + case ObjectType::Enemy: { + // Create a new object of type Enemy + Enemy* enemy = new Enemy(); + + // Push to type-specific enemies vector + typeID = (SpecificID)this->enemies.push(enemy); + + // Push to global objects vector + globalID = (EntityID)this->objects.push(enemy); + + // Set object's type and global IDs + enemy->typeID = typeID; + enemy->globalID = globalID; + break; + } + case ObjectType::Object: { // Create a new object of type Object Object* object = new Object(ObjectType::Object); @@ -84,11 +99,29 @@ SpecificID ObjectManager::createObject(ObjectType type) { // Push to global objects vector globalID = (EntityID)this->objects.push(object); - // Set object's type and global IDs - object->typeID = typeID; - object->globalID = globalID; - break; - } + // Set object's type and global IDs + object->typeID = typeID; + object->globalID = globalID; + break; + } + default: { + // Create a new object of type Object + Object* object = new Object(ObjectType::Object); + + // TODO: Maybe change SmartVector's index return value? size_t is + // larger than uint32 (which is what SpecificID and EntityID are + // defined as) + // Push to type-specific base_objects vector + typeID = (SpecificID)this->base_objects.push(object); + + // Push to global objects vector + globalID = (EntityID)this->objects.push(object); + + // Set object's type and global IDs + object->typeID = typeID; + object->globalID = globalID; + break; + } } // Return new object's specificID diff --git a/src/server/game/player.cpp b/src/server/game/player.cpp index 95473d44..2a963e7a 100644 --- a/src/server/game/player.cpp +++ b/src/server/game/player.cpp @@ -8,7 +8,6 @@ SharedObject Player::toShared() { } Player::Player() : Creature(ObjectType::Player) { - } Player::~Player() { diff --git a/src/server/game/servergamestate.cpp b/src/server/game/servergamestate.cpp index 06ec65b6..ebed4711 100644 --- a/src/server/game/servergamestate.cpp +++ b/src/server/game/servergamestate.cpp @@ -14,6 +14,7 @@ ServerGameState::ServerGameState(GameConfig config) { this->timestep = FIRST_TIMESTEP; this->timestep_length = config.game.timestep_length_ms; this->lobby.max_players = config.server.max_players; + this->lobby.name = config.server.lobby_name; this->maps_directory = config.game.maze.directory; this->maze_file = config.game.maze.maze_file; @@ -29,7 +30,7 @@ ServerGameState::ServerGameState(GamePhase start_phase) this->phase = start_phase; } -ServerGameState::ServerGameState(GamePhase start_phase, GameConfig config) // cppcheck-suppress passedByValue +ServerGameState::ServerGameState(GamePhase start_phase, const GameConfig& config) : ServerGameState(config) { this->phase = start_phase; } @@ -186,9 +187,9 @@ void ServerGameState::updateMovement() { if (object == nullptr) continue; - bool collided = false; - bool collidedX = false; - bool collidedZ = false; + bool collided = false; // cppcheck-suppress variableScope + bool collidedX = false; // cppcheck-suppress variableScope + bool collidedZ = false; // cppcheck-suppress variableScope if (object->physics.movable) { // Check for collision at position to move, if so, dont change position @@ -283,7 +284,7 @@ void ServerGameState::useItem() { SmartVector items = this->objects.getItems(); for (int i = 0; i < items.size(); i++) { - Item* item = items.get(i); + const Item* item = items.get(i); if (item == nullptr) continue; @@ -314,12 +315,8 @@ void ServerGameState::removePlayerFromLobby(EntityID id) { this->lobby.players.erase(id); } -const std::unordered_map& ServerGameState::getLobbyPlayers() const { - return this->lobby.players; -} - -int ServerGameState::getLobbyMaxPlayers() const { - return this->lobby.max_players; +const Lobby& ServerGameState::getLobby() const { + return this->lobby; } /* Maze initialization */ @@ -422,7 +419,8 @@ void ServerGameState::loadMaze() { file.close(); // Verify that there's at least one spawn point - assert(this->grid.getSpawnPoints().size() > 0); + size_t num_spawn_points = this->grid.getSpawnPoints().size(); + assert(num_spawn_points > 0); // Step 5: Add floor and ceiling SolidSurfaces. @@ -474,6 +472,13 @@ void ServerGameState::loadMaze() { GridCell* cell = this->grid.getCell(col, row); switch (cell->type) { + case CellType::Enemy: { + SpecificID enemyID = this->objects.createObject(ObjectType::Enemy); + + Enemy* enemy = this->objects.getEnemy(enemyID); + enemy->physics.shared.position = this->grid.gridCellCenterPosition(cell); + break; + } case CellType::Wall: { // Create a new Wall object SpecificID wallID = diff --git a/src/server/game/solidsurface.cpp b/src/server/game/solidsurface.cpp index 04d2c6db..8f3171e4 100644 --- a/src/server/game/solidsurface.cpp +++ b/src/server/game/solidsurface.cpp @@ -9,9 +9,9 @@ SolidSurface::~SolidSurface() {} /* SharedGameState generation */ SharedObject SolidSurface::toShared() { - SharedObject shared = Object::toShared(); + SharedObject sharedSolidSurface = Object::toShared(); - shared.solidSurface = this->shared; + sharedSolidSurface.solidSurface = this->shared; - return shared; + return sharedSolidSurface; } \ No newline at end of file diff --git a/src/server/lobbybroadcaster.cpp b/src/server/lobbybroadcaster.cpp index 1ae10341..76da4cb1 100644 --- a/src/server/lobbybroadcaster.cpp +++ b/src/server/lobbybroadcaster.cpp @@ -5,6 +5,7 @@ #include #include +#include "shared/game/sharedgamestate.hpp" #include "shared/network/constants.hpp" #include "shared/network/packet.hpp" #include "shared/utilities/config.hpp" @@ -33,9 +34,13 @@ void LobbyBroadcaster::startBroadcasting(const ServerLobbyBroadcastPacket& bcast } } -void LobbyBroadcaster::setLobbyInfo(const ServerLobbyBroadcastPacket& bcast_info) { +void LobbyBroadcaster::setLobbyInfo(const Lobby& lobby_info) { std::unique_lock lock(this->mut); - this->bcast_info = bcast_info; + this->bcast_info = ServerLobbyBroadcastPacket { + .lobby_name = lobby_info.name, + .slots_taken = static_cast(lobby_info.players.size()), + .slots_avail = static_cast(lobby_info.max_players - lobby_info.players.size()) + }; } void LobbyBroadcaster::stopBroadcasting() { diff --git a/src/server/server.cpp b/src/server/server.cpp index d32c631f..7ab2e7d5 100644 --- a/src/server/server.cpp +++ b/src/server/server.cpp @@ -14,6 +14,8 @@ #include #include "boost/variant/get.hpp" +#include "server/game/enemy.hpp" +#include "server/game/player.hpp" #include "shared/game/event.hpp" #include "server/game/servergamestate.hpp" #include "server/game/object.hpp" @@ -33,72 +35,6 @@ Server::Server(boost::asio::io_context& io_context, GameConfig config) world_eid(0), state(ServerGameState(GamePhase::LOBBY, config)) { - /* - EntityID id = state.objects.createObject(ObjectType::Object); - Object* cube = (Object*)state.objects.getObject(id); - cube->physics.shared.position = glm::vec3(0.0f, 0.0f, 5.0f); - cube->physics.shared.corner = glm::vec3(-4.0f, -4.0f, 3.5f); - cube->physics.boundary = new BoxCollider(cube->physics.shared.corner, glm::vec3(8.0f, 8.0f, 3.0f)); - cube->physics.movable = false; - - /* - EntityID floorID = state.objects.createObject(ObjectType::SolidSurface); - SolidSurface* floor = (SolidSurface*)state.objects.getObject(floorID); - floor->shared.dimensions = glm::vec3(20.0f, 0.1f, 20.0f); - floor->physics.shared.corner = glm::vec3(-10.0f, -0.1f, -10.0f); - floor->physics.shared.position = glm::vec3(0.0f, -0.05f, 0.0f); - floor->physics.boundary = new BoxCollider(floor->physics.shared.corner, floor->shared.dimensions); - floor->physics.movable = false;*/ - - /* - // Create a room - /* - EntityID wall1ID = state.objects.createObject(ObjectType::SolidSurface); - EntityID wall2ID = state.objects.createObject(ObjectType::SolidSurface); - EntityID wall3ID = state.objects.createObject(ObjectType::SolidSurface); - EntityID wall4ID = state.objects.createObject(ObjectType::SolidSurface); - EntityID floorID = state.objects.createObject(ObjectType::SolidSurface); - - // Specify wall positions (RECREATED TO MATCH AXIS) - // Configuration: 18 (z) x 20 (x) room example - // (z-axis) - // ##1## - // # # - // 2 3 (x-axis) - // # # - // ##4## - - SolidSurface* wall1 = (SolidSurface*)state.objects.getObject(wall1ID); - SolidSurface* wall2 = (SolidSurface*)state.objects.getObject(wall2ID); - SolidSurface* wall3 = (SolidSurface*)state.objects.getObject(wall3ID); - SolidSurface* wall4 = (SolidSurface*)state.objects.getObject(wall4ID); - SolidSurface* floor = (SolidSurface*)state.objects.getObject(floorID); - - // Wall1 has dimensions (20, 4, 1); and position (0, 0, -19.5) - wall1->shared.dimensions = glm::vec3(20, 4, 1); - wall1->physics.shared.position = glm::vec3(0, 0, -19.5); - wall1->physics.movable = false; - - // Wall2 has dimensions (1, 4, 18) and position (-19.5, 0, 0) - wall2->shared.dimensions = glm::vec3(1, 4, 18); - wall2->physics.shared.position = glm::vec3(-19.5, 0, 0); - wall2->physics.movable = false; - - // Wall3 has dimensions (1, 4, 18) and position (19.5, 0, 0) - wall3->shared.dimensions = glm::vec3(1, 4, 18); - wall3->physics.shared.position = glm::vec3(19.5, 0, 0); - wall3->physics.movable = false; - - // Wall4 has dimensions (20, 4, 1) and position (0, 0, 19.5) - wall4->shared.dimensions = glm::vec3(20, 4, 1); - wall4->physics.shared.position = glm::vec3(0, 0, 19.5); - wall4->physics.movable = false; - - // floor has dimensions (20, 0.1, 20) and position (0, -1.3, 0) - floor->shared.dimensions = glm::vec3(20.0f, 0.1f, 20.0f); - floor->physics.shared.position = glm::vec3(0.0f, -1.3f, 0.0f); - floor->physics.movable = false;*/ - _doAccept(); // start asynchronously accepting if (config.server.lobby_broadcast) { @@ -167,16 +103,21 @@ std::chrono::milliseconds Server::doTick() { } } - if (this->state.getLobbyPlayers().size() >= this->state.getLobbyMaxPlayers()) { + if (this->state.getLobby().players.size() >= this->state.getLobby().max_players) { this->state.setPhase(GamePhase::GAME); + // TODO: figure out how to selectively broadcast to only the players that were already in the lobby + // this->lobby_broadcaster.stopBroadcasting(); } else { - std::cout << "Only have " << this->state.getLobbyPlayers().size() << "/" << this->state.getLobbyMaxPlayers() << "\n"; + std::cout << "Only have " << this->state.getLobby().players.size() + << "/" << this->state.getLobby().max_players << "\n"; } + this->lobby_broadcaster.setLobbyInfo(this->state.getLobby()); + sendUpdateToAllClients(Event(this->world_eid, EventType::LoadGameState, LoadGameStateEvent(this->state.generateSharedGameState()))); // Tell each client the current lobby status - std::cout << "waiting for " << this->state.getLobbyMaxPlayers() << " players" << std::endl; + std::cout << "waiting for " << this->state.getLobby().max_players << " players" << std::endl; break; case GamePhase::GAME: { diff --git a/src/shared/CMakeLists.txt b/src/shared/CMakeLists.txt index 1ebd306c..f2cabe2a 100644 --- a/src/shared/CMakeLists.txt +++ b/src/shared/CMakeLists.txt @@ -9,6 +9,7 @@ set(FILES utilities/config.cpp utilities/rng.cpp utilities/root_path.cpp + utilities/time.cpp ) add_library(${LIB_NAME} STATIC ${FILES}) diff --git a/src/shared/game/sharedobject.cpp b/src/shared/game/sharedobject.cpp index 692498a6..d1521876 100644 --- a/src/shared/game/sharedobject.cpp +++ b/src/shared/game/sharedobject.cpp @@ -4,10 +4,14 @@ std::string objectTypeString(ObjectType type) { switch (type) { case ObjectType::Object: return "Object"; - case ObjectType::Item: - return "Item"; - case ObjectType::SolidSurface: - return "SolidSurface"; + case ObjectType::Item: + return "Item"; + case ObjectType::SolidSurface: + return "SolidSurface"; + case ObjectType::Player: + return "Player"; + case ObjectType::Enemy: + return "Enemy"; default: return "Unknown"; } diff --git a/src/shared/tests/serialize_test.cpp b/src/shared/tests/serialize_test.cpp index f822d7e7..fd979157 100644 --- a/src/shared/tests/serialize_test.cpp +++ b/src/shared/tests/serialize_test.cpp @@ -6,7 +6,7 @@ #include "server/game/servergamestate.hpp" TEST(SerializeTest, SerializeEvent) { - ServerGameState state(GamePhase::LOBBY, GameConfig{}); + ServerGameState state(GamePhase::LOBBY); state.addPlayerToLobby(1, "Player Name"); Event evt(0, EventType::LoadGameState, LoadGameStateEvent(state.generateSharedGameState())); Event evt2 = deserialize(serialize(evt)); @@ -20,7 +20,7 @@ TEST(SerializeTest, SerializeEvent) { } TEST(SerializeTest, SerializePacketEvent) { - ServerGameState state(GamePhase::LOBBY, GameConfig{}); + ServerGameState state(GamePhase::LOBBY); state.addPlayerToLobby(1, "Player Name"); Event evt(0, EventType::LoadGameState, LoadGameStateEvent(state.generateSharedGameState())); diff --git a/src/shared/utilities/config.cpp b/src/shared/utilities/config.cpp index 3b3425c9..b6b59e76 100644 --- a/src/shared/utilities/config.cpp +++ b/src/shared/utilities/config.cpp @@ -1,6 +1,6 @@ #include "shared/utilities/config.hpp" -#include +#include "shared/utilities/root_path.hpp" #include #include @@ -16,11 +16,7 @@ GameConfig GameConfig::parse(int argc, char** argv) { // cppcheck-suppress const exit(1); } - // With the cmake setup we know the executables are three directories down from the root of the - // repository, where the config file lives - boost::filesystem::path root_path = boost::dll::program_location().parent_path().parent_path().parent_path(); - - boost::filesystem::path filepath = root_path / "config.json"; + boost::filesystem::path filepath = getRepoRoot() / "config.json"; if (argc == 2) { filepath = argv[1]; } @@ -58,7 +54,8 @@ GameConfig GameConfig::parse(int argc, char** argv) { // cppcheck-suppress const }, .client = { .default_name = json.at("client").at("default_name"), - .lobby_discovery = json.at("client").at("lobby_discovery") + .lobby_discovery = json.at("client").at("lobby_discovery"), + .window_width = json.at("client").at("window_width") } }; } catch (nlohmann::json::exception& ex) { diff --git a/src/shared/utilities/time.cpp b/src/shared/utilities/time.cpp new file mode 100644 index 00000000..d78b8dd9 --- /dev/null +++ b/src/shared/utilities/time.cpp @@ -0,0 +1,9 @@ +#include "shared/utilities/time.hpp" + +#include + +long getMsSinceEpoch() { + return std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch() + ).count(); +}