Illusion Engine 06 - Window

Introduction

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:

Window.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
#pragma once

#include "Engine/Core/Core.h"
#include "Engine/Event/Events.h"
#include <GLFW/glfw3.h>
//--------------------namespace: Illusion starts--------------------
namespace Illusion
{
struct WindowProps
{
std::string Title;
unsigned int Width;
unsigned int Height;
// Constrctor for the struct WindowProps
WindowProps(const std::string& title = "Illusion Engine",
unsigned int width = 1280,
unsigned int height = 720)
: Title(title), Width(width), Height(height) {}
};
// Window Class
class Window
{
public:
// Creating a template funtion
// Input: Event&
// Output: void
using EventCallbackFn = std::function<void(Event&)>;
Window(const WindowProps& props = WindowProps());
~Window();
void OnUpdate();
inline unsigned int GetWidth() const { return m_Data.Width; }
inline unsigned int GetHeight() const { return m_Data.Height; }
// Set the overall event callback function
inline void SetEventCallback(const EventCallbackFn& callback) { m_Data.EventCallback = callback; }
void SetVSync(bool enabled);
bool IsSync() const;
// Expose the m_Window
inline virtual void* GetNativeWindow() const { return m_Window; }
private:
// Initialize the window properties
virtual void Init(const WindowProps& props);
// Shut the window down
virtual void Shutdown();
private:
GLFWwindow* m_Window;
// Store all the data that a window maintains
struct WindowData
{
std::string Title;
unsigned int 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.

Window.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
#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
static bool s_GLFWInitialized = false;
// A simple Error processor
static void GLFWErrorCallback(int error, const char* 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
void Window::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, unsigned int 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);
});
}
void Window::Shutdown()
{
glfwDestroyWindow(m_Window);
}
void Window::OnUpdate()
{
// Everytime it is updataed, process the event in the queue
glfwPollEvents();
//m_Context->SwapBuffers();
glfwSwapBuffers(m_Window);
}
void Window::SetVSync(bool enabled)
{
if (enabled)
glfwSwapInterval(1);
else
glfwSwapInterval(0);
m_Data.VSync = enabled;
}
bool Window::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:

Applicaton.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#pragma once

#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
{
class Application
{
public:
Application();
virtual ~Application();
//The function where application actually starts
void Run();
// Overall callback function
void OnEvent(Event& event);
void PushLayer(Layer* layer);
void PushOverlay(Layer* overlay);
inline Window& GetWindow() { return *m_Window; };
inline static Application& Get() { return *s_Instance; };
private:
// Callback function that close the window
bool OnWindowClose(WindowCloseEvent& event);
bool OnWindowResize(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;

Application.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
#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(new Window());
// 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() {}
void Application::PushLayer(Layer* layer)
{
// Push the layer into the layerstack and call it to do some prepare work
m_LayerStack.PushLayer(layer);
layer->OnAttach();
}
void Application::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
void Application::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
bool Application::OnWindowClose(WindowCloseEvent& event)
{
m_Running = false;
return true;
}
// The callback function to resize the window
bool Application::OnWindowResize(WindowResizeEvent& event)
{
if (event.GetWidth() == 0 || event.GetHeight() == 0)
{
m_Minimized = true;
return false;
}
m_Minimized = false;
return false;
}
//The function where the app actually starts
void Application::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:

1
2
3
#include "Engine/Core/Application/Application.h"
#include "Engine/Core/Log/Log.h"
#include "Engine/Core/Layer/Layer.h"

Application End

Then, we could use these system in our game application:

TestLayer.h/.cpp

To start our game application, we need to create a layer for our game. In Game/src/… folder, create TestLayer.h/.cpp/ and enter:

TestLayer.h:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#pragma once

#include <Engine.h>
class TestLayer : public Illusion::Layer
{
public:
TestLayer();
virtual ~TestLayer() = default;

virtual void OnAttach() override;
virtual void OnDetach() override;

void OnUpdate() override;
void OnEvent(Illusion::Event& event) override;
};


TestLayer.cpp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include "TestLayer.h"
TestLayer::TestLayer()
:Layer("TestLayer"), m_CameraController(2560.0f / 1440.0f, true)
{}
void TestLayer::OnAttach() {}
void TestLayer::OnDetach() {}
void TestLayer::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);
}
void TestLayer::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"
class Game : public Illusion::Application
{
public:
Game()
{
//PushLayer(new GameLayer());
PushLayer(new TestLayer());
}
~Game() {}
};
//The Creation function for the Game Application
Illusion::Application* Illusion::CreateApplication()
{
return new Game();
}
  • 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.


Illusion Engine 06 - Window
https://rigel.github.io/Window/
Author
Rigel
Posted on
August 21, 2022
Licensed under