In the last article, we implemented the layer system. So in this article, we will create an actual window in the engine and talk about the role of layer system and event system.
Dependency
In order to implement the a window class, we decided to use a library called GLFW, which is usually used to create logic related to windows, events, KeyCode, and the like.
GLFW library has already been included in Illusion/Lib/GLFW… folder and configured in premake.lua file. (The configuration of third-party library is in article Preparation)
Principle
The window itself should have the following properties:
The length and width of itself and the corresponding get() methods;
The Init() function that set up all the requirement for the window’s rendering;
The functions about VSync since we want to control the engine’s performance;
Implementation
In Illusion/src/Engine/Core/Window/… folder, create two files: Window.h/cpp and enter:
#include"Engine/Core/Core.h" #include"Engine/Event/Events.h" #include<GLFW/glfw3.h> //--------------------namespace: Illusion starts-------------------- namespace Illusion { structWindowProps { std::string Title; unsignedint Width; unsignedint Height; // Constrctor for the struct WindowProps WindowProps(const std::string& title = "Illusion Engine", unsignedint width = 1280, unsignedint height = 720) : Title(title), Width(width), Height(height) {} }; // Window Class classWindow { public: // Creating a template funtion // Input: Event& // Output: void using EventCallbackFn = std::function<void(Event&)>; Window(const WindowProps& props = WindowProps()); ~Window(); voidOnUpdate(); inlineunsignedintGetWidth()const{ return m_Data.Width; } inlineunsignedintGetHeight()const{ return m_Data.Height; } // Set the overall event callback function inlinevoidSetEventCallback(const EventCallbackFn& callback){ m_Data.EventCallback = callback; } voidSetVSync(bool enabled); boolIsSync()const; // Expose the m_Window inlinevirtualvoid* GetNativeWindow()const{ return m_Window; } private: // Initialize the window properties virtualvoidInit(const WindowProps& props); // Shut the window down virtualvoidShutdown(); private: GLFWwindow* m_Window; // Store all the data that a window maintains structWindowData { std::string Title; unsignedint Width, Height; bool VSync; EventCallbackFn EventCallback; }; WindowData m_Data; }; //--------------------namespace: Illusion ends-------------------- }
WindowProp is a struct that contains basic information of a window. It simplifies the initialization of the window;
The window would have an EventCallback function. It will be bound to a function that processes all kinds of events in Application class. This functon would be called whenever there’s an event triggered.
GetNativeWindow() is a function that expose the glfwwindow pointer to other classes. It would be used in Input Class.
#include"pch.h" #include"Window.h" #include"Engine/Event/AppEvent.h" #include"Engine/Event/MouseEvent.h" #include"Engine/Event/KeyEvent.h" #include<glad/glad.h> //--------------------namespace: Illusion starts-------------------- namespace Illusion { // Make sure GLFW has been initialized when it is called staticbool s_GLFWInitialized = false; // A simple Error processor staticvoidGLFWErrorCallback(int error, constchar* description) { ENGINE_CORE_ERROR("GLFW ERROR ({0}): {1}", error, description); } //// Create the Window for the program //Window* Window::CreateIllusionWindow(const WindowProps& props) //{ // return new Window(props); //} // The constructor of the Window class // Initialize the properties of the window Window::Window(const WindowProps& props) { Init(props); } // The destructor of the Window class Window::~Window() { Shutdown(); } // Initialization of the properties of the window voidWindow::Init(const WindowProps& props) { // Unpack the data input // Store all the data that window maintains m_Data.Title = props.Title; m_Data.Width = props.Width; m_Data.Height = props.Height; // Log the data ENGINE_CORE_INFO("Window Created: {0} ({1},{2})", m_Data.Title, m_Data.Width, m_Data.Height); // Initialize GLFW if (!s_GLFWInitialized) { int success = glfwInit(); ILLUSION_CORE_ASSERT(success, "Could not intialize GLFW!"); // Set up the error callback function glfwSetErrorCallback(GLFWErrorCallback); s_GLFWInitialized = true; } // Create the window with the data that the window maintains m_Window = glfwCreateWindow((int)props.Width, (int)props.Height, m_Data.Title.c_str(), nullptr, nullptr); // Set up the graphic context glfwMakeContextCurrent(m_Window); // Check if glad has been initialized int status = gladLoadGLLoader((GLADloadproc)glfwGetProcAddress); ILLUSION_CORE_ASSERT(status, "Failed to initialize Glad!"); // Log the OpenGL information to the console ENGINE_CORE_INFO("OpenGL Info:"); ENGINE_CORE_INFO(" Vendor\t: {0}", glGetString(GL_VENDOR)); ENGINE_CORE_INFO(" Renderer\t: {0}", glGetString(GL_RENDERER)); ENGINE_CORE_INFO(" Version\t: {0}", glGetString(GL_VERSION)); // The user pointer could be used to store whatever you what glfwSetWindowUserPointer(m_Window, &m_Data); SetVSync(true); // Set GLFW callbacks // Use Lambda expressions that generate a callable funtion and set it to be the callback function glfwSetWindowSizeCallback(m_Window, [](GLFWwindow* window, int width, int height) { // Retrieve the data that we stored in the user pointer WindowData& data = *(WindowData*)glfwGetWindowUserPointer(window); // Store the change data.Width = width; data.Height = height; WindowResizeEvent event(width, height); data.EventCallback(event); }); glfwSetWindowCloseCallback(m_Window, [](GLFWwindow* window) { WindowData& data = *(WindowData*)glfwGetWindowUserPointer(window); WindowCloseEvent event; data.EventCallback(event); }); glfwSetKeyCallback(m_Window, [](GLFWwindow* window, int key, int scancode, int action, int mods) { WindowData& data = *(WindowData*)glfwGetWindowUserPointer(window); switch (action) { case GLFW_PRESS: { KeyPressedEvent event(key, 0); data.EventCallback(event); break; } case GLFW_RELEASE: { KeyReleasedEvent event(key); data.EventCallback(event); break; } case GLFW_REPEAT: { KeyPressedEvent event(key, 1); data.EventCallback(event); break; } } }); glfwSetCharCallback(m_Window, [](GLFWwindow* window, unsignedint keycode) { WindowData& data = *(WindowData*)glfwGetWindowUserPointer(window); KeyTypedEvent event(keycode); data.EventCallback(event); }); glfwSetMouseButtonCallback(m_Window, [](GLFWwindow* window, int button, int action, int mods) { WindowData& data = *(WindowData*)glfwGetWindowUserPointer(window); switch (action) { case GLFW_PRESS: { MouseButtonPressedEvent event(button); data.EventCallback(event); break; } case GLFW_RELEASE: { MouseButtonReleasedEvent event(button); data.EventCallback(event); break; } } }); glfwSetScrollCallback(m_Window, [](GLFWwindow* window, double xOffset, double yOffset) { WindowData& data = *(WindowData*)glfwGetWindowUserPointer(window); MouseScrolledEvent event((float)xOffset, (float)yOffset); data.EventCallback(event); }); glfwSetCursorPosCallback(m_Window, [](GLFWwindow* window, double xPos, double yPos) { WindowData& data = *(WindowData*)glfwGetWindowUserPointer(window); MouseMovedEvent event((float)xPos, (float)yPos); data.EventCallback(event); }); } voidWindow::Shutdown() { glfwDestroyWindow(m_Window); } voidWindow::OnUpdate() { // Everytime it is updataed, process the event in the queue glfwPollEvents(); //m_Context->SwapBuffers(); glfwSwapBuffers(m_Window); } voidWindow::SetVSync(bool enabled) { if (enabled) glfwSwapInterval(1); else glfwSwapInterval(0); m_Data.VSync = enabled; } boolWindow::IsSync()const { return m_Data.VSync; } //--------------------namespace: Illusion ends-------------------- }
GLFWErrorCallback() is a function that would be called when glfw is having trouble with the window;
In the Init() function, the data passed in will be unpacked and used to initialize the window. Then, we initialize GLFW, glad, and graphic context;
The UserPointer is a void pointer that could be used to store whatever data we want and the data stored in it could be read by using glfwGetWindowUserPointer(). In this case, we use this pointer to store the window’s data, precisely the EventCallback function;
For every glfwSet___Callback(), we pass in the current window and a lambda expression. These lambda expressions work like anonymous functions. The function would bind these them to the glfw callback functions. They would unpack the data stored in the UserPointer and take out the EventCallback function and call it;
Here we bind all types of callback functions to corresponding lambda expressions, so that we can handle all types of events;
OnUpdata() function is where we refresh the window and update the frame;
Utilization
So far, we have implemented every thing that we need to render a window. Next step we have to revise the exsisting code in order to put these systems into use.
Engine End
Firstly, we have to revise the code in Engine end:
#include"Engine/Core/Core.h" #include"Engine/Core/Layer/LayerStack.h" #include"Engine/Event/Events.h" #include"Engine/Event/AppEvent.h" #include"Engine/Core/Window/Window.h" //--------------------namespace: Illusion starts-------------------- namespace Illusion { classApplication { public: Application(); virtual ~Application(); //The function where application actually starts voidRun(); // Overall callback function voidOnEvent(Event& event); voidPushLayer(Layer* layer); voidPushOverlay(Layer* overlay); inline Window& GetWindow(){ return *m_Window; }; inlinestatic Application& Get(){ return *s_Instance; }; private: // Callback function that close the window boolOnWindowClose(WindowCloseEvent& event); boolOnWindowResize(WindowResizeEvent& event); private: std::unique_ptr<Window> m_Window; bool m_Running = true; bool m_Minimized = false; LayerStack m_LayerStack; private: static Application* s_Instance; }; //The Creation function of the application //Should be implement by the user themselves //Since we don't know what they will call their apps and what they will do with their apps //There cannot be a uniform implementation of the creation function //It is implemented in the Game.cpp Application* CreateApplication(); //--------------------namespace: Illusion ends-------------------- }
We use a unique pointer to store our window instance.
OnEvent() function would be bound to the window’s EventCallback function. The function will be used to dispatch all the event to every layers and window itself;
PushLayer() and PushOvelayer() are used to push layers into the layerstack;
GetWindow() and Get() expose the window instance and application instance to other classes such as Input;
OnWindowClose() and OnWindoResize() are callback functions that work with m_Running and m_Minimized to determine the status of the window and application;
#include"pch.h" #include"Application.h" #include"Engine/Core/Log/Log.h" //--------------------namespace: Illusion starts-------------------- namespace Illusion { Application* Application::s_Instance = nullptr; Application::Application() { ILLUSION_CORE_ASSERT(!s_Instance, "Application already exists!"); s_Instance = this; // Create a window m_Window.reset(newWindow()); // Bind OnEvent as a overall callback function glfw // The program would call OnEvent whenever there's an event m_Window->SetEventCallback(ENGINE_BIND_EVENT_FN(Application::OnEvent)); } Application::~Application() {} voidApplication::PushLayer(Layer* layer) { // Push the layer into the layerstack and call it to do some prepare work m_LayerStack.PushLayer(layer); layer->OnAttach(); } voidApplication::PushOverlay(Layer* overlay) { // Push the overlay layer into the layerstack and call it to do some prepare work m_LayerStack.PushOverlay(overlay); overlay->OnAttach(); } // The overall callback function voidApplication::OnEvent(Event& event) { // Create a dispatcher which is bound to event EventDispatcher dispatcher(event); // If the event is OnWindowClose, the dispatcher would dispatch it and call the OnWindowClose function dispatcher.Dispatch<WindowCloseEvent>(ENGINE_BIND_EVENT_FN(Application::OnWindowClose)); dispatcher.Dispatch<WindowResizeEvent>(ENGINE_BIND_EVENT_FN(Application::OnWindowResize)); for (auto it = m_LayerStack.end(); it != m_LayerStack.begin();) { (*(--it))->OnEvent(event); if (event.m_Handled) break; } } // The callback function to close the window boolApplication::OnWindowClose(WindowCloseEvent& event) { m_Running = false; returntrue; } // The callback function to resize the window boolApplication::OnWindowResize(WindowResizeEvent& event) { if (event.GetWidth() == 0 || event.GetHeight() == 0) { m_Minimized = true; returnfalse; } m_Minimized = false; returnfalse; } //The function where the app actually starts voidApplication::Run() { while (m_Running) { if (!m_Minimized) { // Update Objects in the game base on the Layer order for (Layer* layer : m_LayerStack) layer->OnUpdate(); } // Update everything m_Window->OnUpdate(); } } //--------------------namespace: Illusion ends-------------------- }
PushLayer() and PushOverlay() will push the layers into the layerstack and call their OnAttach() functons;
OnEvent() dispatches the events passed in to OnWindowClose() and OnWindowResize(); Then it would iterate through the layerstack from back to front to call each layer’s OnEvent() function;
The order of layers in the layerstack determines the rendering order. But the logic order of layers is reversed from their rendering order. When we click a certain position on the screen, if there is a debug button, we definitely don’t want the in-game UI below to be clicked, and we also don’t want the character to make an attack action. Therefore, we need to reverse the rendering order and check whether each Layer can respond to this event. And decide whether to consume this event base on the behavior of the corresponding response (whether to stop here, or continue to the next Layer to respond)
By using the bool m_Minimized, we could stop updating the window when it is minimized.
Engine.h
Inside Engine.h, we have to add some include command in order to expose our systems to the game application. The file would be like this:
#include"TestLayer.h" TestLayer::TestLayer() :Layer("TestLayer"), m_CameraController(2560.0f / 1440.0f, true) {} voidTestLayer::OnAttach(){} voidTestLayer::OnDetach(){} voidTestLayer::OnUpdate(Illusion::Timestep timestep) { // Render // Set the clear color // Clear the color buffer with the clear color glClearColor(0.3f, 0.3f, 0.3f, 1.0f); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); } voidTestLayer::OnEvent(Illusion::Event& event){}
The TestLayer class inherites from Layer class;
Currently we have nothing to do in OnAttach(), OnDetach(), and OnEvent() functons;
OnUpdate() function would set a color that would be used to refresh the background of the window, and glClear() would clear the buffer with that color.
Game.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
#include<Engine.h> //--------------------Entry point for the application-------------------- #include<Engine/Core/EntryPoint.h> #include"TestLayer.h" classGame : public Illusion::Application { public: Game() { //PushLayer(new GameLayer()); PushLayer(newTestLayer()); } ~Game() {} }; //The Creation function for the Game Application Illusion::Application* Illusion::CreateApplication() { returnnewGame(); }
Here we create an instance of the TestLayer and push it into the layerstack so the application could update it automatically.
Conclusion
So far, we have finished the implementation of our first window and the utilization of the event system and layer system. Next step, we are going to create several helper classes such as a class that would generate a debugging window, a class that could set the update rate fixed, a class that could process the input, etc.