Of the Moon and the Sun
We choose to go to the moon in this decade and do the other things, not because they are easy, but because they are hard.
— John F. Kennedy
To be able to read in configuration files and to run scripts later on, we will learn how to use Lua. This tutorial is just an introduction, but when writing code to drive gameplay, it is becoming more and more common to use a scripting language.
Lua
Lua is Portuguese for moon. Lua is also a powerful, yet lightweight, embeddable scripting language, created in 1993 by a team of computer scientists at the Pontifical Catholic University of Rio de Janeiro and it so happens to be the most popular general-purpose scripting language used in games today.
It is dynamically typed, runs by interpreting bytecode with a register-based virtual machine and has automatic memory management with incremental garbage collection, making it ideal for configuration and scripting.
A famous game using Lua is World of Warcraft by Blizzard. What makes Lua so interesting for games is that Lua is the fastest language in the realm of interpreted scripting languages. Furthermore, adding Lua to an application does not bloat it; the reference C implementation is only about
Okay, so Lua is a fast scripting language engine with small footprint that can be embedded easily into our game. So let us do just that.
Installation
To start, just download the x64 binaries from the Lua website and unpack the contents into a meaningful directory, for example P:\Lua\x64.
From now on, we will configure our game as a
Once done, make sure to always compile the project for the
Including the Lua library to our project is now as easy as this:
To avoid the .dll hell, simply copy the lua53.dll
into the working directory of your project. To check if Lua is working, try this minimal example:
For more information about Lua, check the documentation. As said in the introduction, we will learn more about Lua in later tutorials.
Sol
Sol is Portuguese for sun. Sol is also the go-to framework for high-performance binding between Lua and C++, with an easy-to-use API.
Installation
To get Sol, simply download the header file and copy it into the Lua include folder.
In later tutorials, we will learn how to fully harness the power of Lua and Sol, but for now, we will be contempt with simply reading the game settings, in this case, the desired screen resolution, from a configuration file on the disk.
Reading a Configuration File
As a first demonstration of the power of the moon and the sun, we will create a window with the desired resolution read in from the configuration file bell0prefs.lua
in the MyDocuments/bell0bytes/bell0tutorials/Settings folder.
Cleaning Up
To hide as much of the dirty work from the actual game as possible, the creation of the logger service was moved to the DirectXApp class.
In addition, since now, also configuration files have to be read from the disk, the DirectXApp stores the path to the My Documents folder, as well as to the Log and Settings folders of this tutorial. And obviously the logger constructor won’t duplicate that code.
I won’t bother you with the details of all the changes, but please do not be alarmed when noticing that the source code for this tutorial has changed quite a bit from the last tutorial. All of the initialization is now handled by the DirectXApp class:
The checkConfigurationFile function checks for a valid configuration file: If the file is not found or if it is empty, the file is created with default settings (resolution:
Utility
One thing that is a bit of a hassle is that Sol, or Lua, expect a std::string as input for the name of a file to open, thus a StringConverter class is needed:
For details on how these functions work, please look here. We will see how to make use of them in just a moment.
The Power of the Sun
Now with all of that out of the way, it is finally time to create a window with a given size instead of CW_USEDEFAULT. For that purpose, the Window class now has two member variables to store the desired screen resolution and a new private function to load those variables from a file:
And here is how to read the configuration file:
It was promised that Lua and Sol are lightweight and easy to use, and behold, those promises were true.
sol::state lua
The most important Sol class is probably the sol::state_view. This structure takes a pointer to an already existing lua_State and enables simple access to the Lua interface.
sol::state derives from sol::state_view, inheriting all of the functionality, but it has the additional purpose of creating a fresh pointer to a lua_State and managing its lifetime by itself. Thank you, Sun - you truly are the enabler of life on Earth!
lua.script_file(const std::string &)
This function simply opens the file specified by the std::string. This is where the new StringConverter class comes into play; we can now easily convert between std::string and std::wstring. (Since Windows 10 uses Unicode natively, it is wise to keep using wide strings in our project.)
The lua.script_file function throws errors, thus in the actual code it is in a try-block. In this case, if the opening of the configuration file fails, the application starts with its default settings.
Now the true power lies in how easy it truly is to read in the variables.
Here is the file Lua is supposed to read from:
The sol::state behaves exactly like a table. The syntax to get nested variables from the configuration file, is the same as for accessing multidimensional arrays:
The get_or function either returns the value read from the configuration file (i.e., the value stored under config, then resolution, then width, which is 800), or the specified value if something went wrong.
Actually, the table is the only complex data type supported by Lua, but it can be used in various ways, as an array, a multidimensional array (as seen above), or even as a dictionary:
Now this is all pretty easy, yet powerful, thus quite exciting! But before we can create a window with the desired resolution, there is one last thing to consider:
Window Size versus Client Size
When working with windows, it is important to know the exact dimensions of the area that can be tampered with. It is clear that if, for example, a request is made for a
The first parameter is a long pointer to a rectangle structure that contains the coordinates of the top-left and bottom-right corners of the desired client area. When the function returns, the structure contains the coordinates of the top-left and bottom-right corners of the window to accommodate the desired client area.
The second parameter defines the style of the window whose required size is to be calculated.
The third parameter indicates whether the window has a menu or not.
The last parameter defines the extended window style of the window whose required size is to be calculated.
The return value of the function is a bool.
Here is an example to compute the window size of an overlapped window, with no menu, and with the desired client area read from the Lua configuration file:
The desired with and height are stored in the clientWidth and clientHeight member variables, and the AdjustWindowRect function then calculates the required window size. This information, the width being
Putting It All Together
As already mentioned, all the initialization is now done by the DirectXApp class, thus the new WinMain is only concerned about actually doing game related stuff:
Please have a look at the source code to notice the change from the last tutorial.
Behold, a wild
And here is the log file:
Exercise
Play around with the configuration file to create windows of different sizes. Can you feel the power of being able to change the configuration of your application without having to recompile everything?!
References
- Lua
- Microsoft Developer Network
- Sol
- Tricks of the Windows Game Programming Gurus, by André LaMothe