Illusion Engine 03 - Log System

Introduction

To have better support for the debugging of our game engine, it is necessary to have helper modules such as a logging system built before we start to code.

A log system has many functions. It can record everything that happened to the program, which help us get rid of so many breakpoints and easily locate the problem just by analyzing the log.

In addition, when we just want to confirm whether a certain function is called, the log system allows us to know the result without interrupting the program.

Most importantly, the log system helps us distinguish the severity of the information. For example, warning information is displayed in yellow, error information is displayed in red, etc.


Implementation

Although writing a customized log library is not that hard, I decided to use SpdLog library which is a fast C++ log library to build our log system. The library is in Illusion/Lib/… folder and configured in premake.lua file.(The configuration of third-party library is in article Preparsation)

Log.h

To build the log system, create a .h file called Log.h in Illusion/src/Engine/Core/Log/… folder and enter code:

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
#pragma once

#include "Engine/Core/Core.h"
#include "spdlog/spdlog.h"
#include "spdlog/fmt/ostr.h"

//--------------------namespace: Illusion starts--------------------
namespace Illusion
{
class Log
{
public:
//Initialize the logging system
//Set up loggers for both engine and application
static void Init();

inline static std::shared_ptr <spdlog::logger>& GetCoreLogger() { return s_CoreLogger; };
inline static std::shared_ptr <spdlog::logger>& GetClientLogger() { return s_ClientLogger; };

private:
//Two loggers: Logger for engine and logger for application
static std::shared_ptr <spdlog::logger> s_CoreLogger;
static std::shared_ptr <spdlog::logger> s_ClientLogger;


};
//--------------------namespace: Illusion ends--------------------
}

//Core Log Macros
#define ENGINE_CORE_TRACE(...) ::Illusion::Log::GetCoreLogger()->trace(__VA_ARGS__)
#define ENGINE_CORE_INFO(...) ::Illusion::Log::GetCoreLogger()->info(__VA_ARGS__)
#define ENGINE_CORE_WARN(...) ::Illusion::Log::GetCoreLogger()->warn(__VA_ARGS__)
#define ENGINE_CORE_ERROR(...) ::Illusion::Log::GetCoreLogger()->error(__VA_ARGS__)

//Client Log Macros
#define ENGINE_CLIENT_TRACE(...) ::Illusion::Log::GetClientLogger()->trace(__VA_ARGS__)
#define ENGINE_CLIENT_INFO(...) ::Illusion::Log::GetClientLogger()->info(__VA_ARGS__)
#define ENGINE_CLIENT_WARN(...) ::Illusion::Log::GetClientLogger()->warn(__VA_ARGS__)
#define ENGINE_CLIENT_ERROR(...) ::Illusion::Log::GetClientLogger()->error(__VA_ARGS__)
  • For Illuison Engine, I chose to use a singleton static log system that is (due to its static nature) always available throughout the project.
    • Using a singleton class with static functionality has several advantages and disadvantages, with its disadvantages mostly being the loss of several OOP properties and less control over construction/destruction. However, for relatively small projects like this it is easy to work with.
  • The difference between CoreLogger and ClientLogger is where the Log occurs. Generally, all logs from the engine should be CoreLog, and all logs from the Application should be ClientLog.
  • Then we only need to encapsulate some corresponding Macros:
1
#define ENGINE_CORE_TRACE(...)			::Illusion::Log::GetCoreLogger()->trace(__VA_ARGS__)
  • Here, (…) represents the Omit parameter inside, and __ VA_ARGS __ will pass the content inside to the corresponding function.
  • :: is the Scope Resolution Operator. Adding a double colon before Illusion ensures that no matter where it is called, it will be searched from the Global Scope.

Log.cpp

The code inside Log.cpp looks like this:

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
#include "pch.h"
#include "Log.h"
#include "spdlog/sinks/stdout_color_sinks.h"

//--------------------namespace: Illusion starts--------------------
namespace Illusion
{
std::shared_ptr <spdlog::logger> Log::s_CoreLogger;
std::shared_ptr <spdlog::logger> Log::s_ClientLogger;

void Log::Init()
{
//Set up all the logging pattern
spdlog::set_pattern("%^[%T] %n: %v%$");

//Set up the Logger for game engine
s_CoreLogger = spdlog::stdout_color_mt("ENGINE");
s_CoreLogger->set_level(spdlog::level::trace);

//Set up Logger for game application
s_ClientLogger = spdlog::stdout_color_mt("APP");
s_ClientLogger->set_level(spdlog::level::trace);
}
//--------------------namespace: Illusion ends--------------------
}
  • At the start of Log.cpp, s_CoreLogger and s_ClientLogger are initialized. Since they are static members in Log class, if they are not initialized, an unresolved external link problem (LNK2001) will be caused when calling GetCoreLogger() or GetClientLogger().
  • The format of the Log is set by following code. %^ to %$ is the Color Range, the text color in this range will be different, [%T] gives us the current time in HH:MM:SS format, %n is the name of the current logger, %v is the actual text content.
1
2
//Set up all the logging pattern
spdlog::set_pattern("%^[%T] %n: %v%$");

Use in Project

EntryPoint.h

To use the log system in our project, we could just initialize it at the start of our program:

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

extern Illusion::Application* Illusion::CreateApplication();

//The main function for the whole application
int main(int argc, char** argv)
{
//Initialization of the Logging system
Illusion::Log::Init();
ENGINE_CORE_WARN("--------------------Logging System Initialized--------------------");

//Create the Game app
auto app = Illusion::CreateApplication();

//Start the Game app
app->Run();

//Delete the Game app
delete app;

return 0;
}

As the result, a log informatin would be printed in the console:

Engine.h

Our last step is to include the Log system in Engine.h. So that our application could use it wherever we want.

1
2
3
4
5
#pragma once

// For use in applications
#include "Engine/Core/Application/Application.h"
#include "Engine/Core/Log/Log.h"

Conclusion

So far, we have completed the Log System. Where needed in the future, we could simply use code like this:

1
ENGINE_CORE_INFO("Window Created: {0} ({1},{2})", m_Data.Title, m_Data.Width, m_Data.Height);

to get whatever information we want.


Illusion Engine 03 - Log System
https://rigel.github.io/LogSystem/
Author
Rigel
Posted on
August 2, 2022
Licensed under