Episode 2 of a series about building an MMO from scratch, with the focus on the engineering around the game. We start the C++ client: why raylib, a window and a frame loop, Dear ImGui through rlImGui, and our own menu. We render frames into a texture to test the renderer. The test passes on the Mac and fails in Docker, as there's no display to open a window on, so we give it a virtual one with Xvfb, and render on the CPU with Mesa's llvmpipe. Then we set up a 3D camera, write our first shaders in GLSL to light a cube for our character, lay down a ground of tiles, and move the cube with the keyboard. A test for the ground catches the pixels we read back coming out upside down, and we go over why.
Topics: Graphics
The code
Each step on screen is a tag in the game's repository. The code at the end of the episode is ep002/end.
- 5:02
ep002/step-1A window with raylibBrowseChanges - 14:52
ep002/step-2A window with raylibBrowseChanges - 17:31
ep002/step-3Dear ImGuiBrowseChanges - 24:12
ep002/step-4Our own menuBrowseChanges - 31:10
ep002/step-5Rendering into a textureBrowseChanges - 36:38
ep002/step-6Testing the rendererBrowseChanges - 41:27
ep002/step-7No screen in DockerBrowseChanges - 45:01
ep002/step-8Drawing a cubeBrowseChanges - 55:37
ep002/step-9A ground and a moving cubeBrowseChanges - 1:03:01
ep002/step-10Upside downBrowseChanges - 1:04:20
ep002/step-11DocsBrowseChanges
Transcript
Intro
0:00 Hello and welcome back to episode two of the nanommo project, where we're going to start on the client side of the game. Let's begin with a quick look at where we're going to be by the end of the episode.
0:13 As you can see, the client draws a cube with lighting on a plane with tiles on it, using raylib, and the keyboard allows us to move it around. It has a menu and interface built with Dear ImGui. And its tests can render frames in Docker, even without a screen and no GPU, which is useful for automated testing on a remote server.
0:33 On the website of the series you can find more information about the project. The code for the project is open source on GitHub, tagged at each step of each episode so that you can follow along.
Last time
0:45 Now, before we get to today's work, let's quickly go over what we did last time. In the first episode, we went over the plan for the series and its architecture. The architecture is a native client in C++, drawn with raylib, and an authoritative server, also in C++. Redis and MySQL are going to come in behind it, once the game needs them. We then started the server, with a single function that turns a tick rate into the time between two ticks. The server's code follows Google's C++ style guide, which clang-format and cpplint check for us. And errors come back as values, with Abseil's status, as the guide doesn't use exceptions. CMake builds it, and GoogleTest tests it, so we know the tick interval comes out right. And our tests caught a rounding bug right away, which shows why tests are valuable. So we write them even without test-driven development, which would need us to write them before the code. Docker builds and tests all of it on Ubuntu, and gives us a small image that runs the server. And GitHub Actions does that whenever we push, and publishes the API docs generated from our code comments.
Today
1:57 Today, we're going to start on the client, all the way from an empty window to a cube that we can move around. We begin with a window, opened with a library called raylib, which also takes care of talking to the graphics card. And a frame loop clears the window to a colour, at the start of each frame. On top of that, we add Dear ImGui, a library for building user interfaces inside the game, and make our own menu with it. Then, like we did with the server, we're going to test it. And we're going to run that test in Docker, where there's no screen and no graphics card at all. In the second half, we write our first shaders, and draw a cube, which is our character for now. It stands on a ground made of tiles, and by the end, we can move it around with the keyboard. All right, let's get started with how we're going to draw.
Why raylib
2:45 To draw, we're going to use a library called raylib, and there are a few alternatives. One is to talk to a graphics API directly, and most of my own graphics work so far has been with OpenGL. It's quite easy to start with, as the driver does a lot of the work behind the scenes. Vulkan is the modern, cross-platform one, and it's a lot more explicit, which gives us more control over performance. However, it takes a few hundred lines of setup before the most basic shape can be drawn, so we will simplify, at least for now. At the other end, there are engines, such as Unity, Unreal and Godot. They give us a lot, but they come with an editor and a way of working of their own. And the engineering we want to look at would end up hidden inside them. raylib sits in between, as a C library that opens the window and reads the keyboard and the mouse. It draws in 2D and 3D in a handful of calls, and behind the scenes, it uses OpenGL to do it. This series is about the engineering around the game, so I want the graphics code to stay small. That way, we spend our time on the server, the tests and the deployment. And when we do want to go deeper, raylib has a lower layer, rlgl, which is a lot closer to OpenGL. Drawing with OpenGL does tie us to an API that Apple stopped updating years ago, as it wants Mac apps to use Metal, its own graphics API. OpenGL still works on today's Macs, and raylib needs no more than version three point three of it. If that ever changes, raylib also supports OpenGL ES, which layers such as Google's ANGLE run on top of Metal, and that keeps our options open.
4:25 raylib is by Ramon Santamaria, and it's been around for over ten years. It's a simple and easy-to-use library to make game programming more fun and straightforward. raylib has no external dependencies, as it comes with the libraries it uses, such as GLFW for the window. It runs on Windows, Linux and macOS, along with Android and the web. And it draws with OpenGL, from quite old versions up to four point three. Its basic example is the whole loop of a raylib program, from init window to end drawing, and our client is going to start out a lot like it.
A window with raylib
5:02 We start in the top-level CMakeLists, where we bring in raylib. We fetch it with FetchContent, like we did with Abseil and GoogleTest in episode one, so nobody has to install raylib by hand. The URL points at release six point oh on GitHub, pinned like our other dependencies. And system keeps the warnings from raylib's headers out of our build, like it does for Abseil. Make available then downloads it, and adds raylib's targets to our build, so we can link against them by name. Further down, add subdirectory pulls in a new client folder, next to the server. So one configure and one build give us both programs. The client gets a CMakeLists of its own, in that folder. For now, it's the client's executable, built from main dot c c, where the frame loop is going to go. The target include directories function adds the project's root folder, so that we can include our headers by their path from the root. Target link libraries links it with raylib, and as linking a CMake target brings in its include folders too, we get raylib's headers without listing them. Then come three of Abseil's libraries: status and status or, for errors, like in the server, and memory, for a helper we're going to need in the window. Both calls are private, as nothing links against the client executable. Next, the window header opens with a header guard named after its path, like our other headers, as we saw in episode one. Then memory, the standard header for smart pointers, as the window is going to be handed out in one. Then string, the standard library's text type, for the window's title. And status or, from Abseil, for returning a window or an error. Everything goes in our nanommo namespace, like the server's code, so our names can't clash with raylib's. We wrap the window in a class of our own. raylib gives us init window and close window, and it's easy to forget the second, or to skip it when we return early. So our class is going to open the window, and its destructor is going to close it. However, opening a window can fail, say on a machine without a display. A constructor can't hand back an error, as it has no return value, and Google's style guide doesn't use exceptions. So the class gets a factory instead, which is a function that makes the object for us. It's a static function called create, and static means it belongs to the class, not to an instance of the window. So we can call it before there is a window. It takes the title by const reference, which lets create read the caller's string without copying it, or changing it. Then come the width and the height, and it returns a status or of a unique pointer to a window. That's the window, if it opened, or an error that says why it didn't. A unique pointer owns an object on the heap, and frees it when the pointer goes away, so whoever holds the pointer owns the window. Then the destructor closes the window, and in C++, a destructor runs when its object goes away, whichever way the code around it finishes. So the window lives as long as the object does, and we can't forget to close it. The next two lines are the copy constructor and the copy assignment, set to equals delete. That tells the compiler not to generate them, as it would write them for us by default. With them deleted, copying a window doesn't compile, so we can't end up with two copies that both try to close the one window when they're destroyed. Then should close tells the frame loop when the user has asked to close the window. It's marked const, which means calling it doesn't change our object. Last, under private, goes the constructor, so code outside the class can't call it. The way to get a window is through create, which checks that it opened. The constructor is set to equals default, which asks the compiler to write it for us. It has nothing to do, as create has opened the window by the time it runs. The source file starts with our own header, which the style guide puts first in each source file. Then raylib dot h, raylib's header, which declares all of its functions. We include it here, and not in our header, so that the code that uses our window doesn't pull all of raylib in as well. Next come memory and string again, which our header has already, as each file includes the standard headers it uses, like in episode one. Then Abseil's memory, for wrap unique, which we're about to use. Then Abseil's status, for its errors, while status or comes with our header. Now we write create, and its return type comes first, the status or of a unique pointer to a window. Its name is Window colon colon create, as we're writing it outside the class, so the compiler knows which class it belongs to. Set trace log level turns raylib's logging down to warnings and errors. By default, it prints each step of its start-up, and we don't need to read that each time we run the client. Set config flags takes settings for the window, and they have to come before it opens. Flag window high D P I scales the window up on high-DPI screens, such as a Retina display, so that it doesn't come out tiny. Init window then opens it, with our size and title, along with the OpenGL context that draws into it. C str hands it the title as a plain C string, as raylib is a C library. Init window doesn't return an error, so we call is window ready afterwards, to check that the window opened. If it isn't, create returns an unavailable error, and no window object gets made. Unavailable is one of Abseil's error codes, for something we need that isn't there, here a display to open the window on. Next, set exit key, with key null, which switches off the exit key. The reason goes in a comment above it: raylib closes the window on Escape, while in a game, Escape closes menus. Then set target FPS caps the loop at sixty frames a second. Without it, the loop would draw as fast as it can, and keep a whole CPU core busy drawing frames the screen can't show. Last, with the window open, we make the object, where new makes a window on the heap and hands us a plain pointer to it. Wrap unique, from Abseil's memory, puts that pointer in a unique pointer, so the window has an owner right away. The standard library's make unique can't do this, as it builds the object from outside our class, where the constructor is private. Create is part of the class, so it can call the constructor, and the status or it returns holds the window. The destructor calls close window, and should close calls raylib's window should close, which turns true when the user clicks the window's close button. Now for main, which includes raylib dot h, as it does the drawing for now. Then iostream, to print an error, and memory, for the unique pointer. And utility, for standard move, which we're going to need in a moment. Then status or, for what create returns, and our window header. In main, we start with a status or of a unique pointer to a window, which is what create returns. It may hold an error instead of a window, so we call it window or. That suffix is a habit from Google's code, for a value that may be an error. We fill it in with create, for a window called nanoMMO, at twelve eighty by seven twenty. If it isn't OK, we print its status to standard error, with the client's name in front, and return one. That's how the server's main handles an error from its tick interval as well. Standard error is the stream for problems, so the message still shows up when the normal output goes to a file or to another program. Otherwise, we take the window out of window or, and we have to move it out. A unique pointer can't be copied, as the window would then have two owners. Standard move says we're done with window or, and the star hands us the pointer inside it. Our window pointer is const, as it's going to hold that window until main returns. When it does, the unique pointer deletes the window, and the destructor closes it. Then comes the frame loop, and while the window shouldn't close, we draw a frame. The arrow calls should close on the window, through the pointer that owns it. Begin drawing starts the frame, and clear background fills it with a dark blue. raylib's colour is red, green, blue and alpha, each a byte from zero to two fifty five, and an alpha of two fifty five means fully opaque. End drawing finishes the frame and shows it, and handles the keyboard and mouse events that came in. It also waits as long as it takes to keep us at sixty frames a second. Last, the Dockerfile needs a couple more packages, as that's where CI builds everything. On Linux, raylib builds GLFW from source, and GLFW needs the headers for X11, which is how Linux desktops show windows. raylib needs the OpenGL headers as well, so the toolchain stage installs x org dev and lib G L one mesa dev. Down in the lint stage, find now looks in the client folder too, for clang-format to check. And cpplint gets the client folder, next to the server, so the client's code follows the style guide, like the server's.
14:07 Let's build it, which takes a little longer this first time, as the build also compiles raylib and the libraries it comes with. And at the very end, linking the client fails, with a list of undefined symbols: window create, the destructor and should close, each referenced from main's object file. Main compiled, as it includes our window header, and the header declares those functions. However, the code that defines them is in window dot c c, and we didn't add it to the client in CMake, so it wasn't compiled at all. The compiler turns each source file into an object file on its own, and the linker then joins them into the program. So a missing source file goes unnoticed until the linker looks for the functions that were in it.
14:53 So, let's go back to the client's CMakeLists and fix the build. Add executable lists the source files that make up the client, and so far, it lists main dot c c and nothing else. We add window dot c c after it, so CMake compiles it into the client, next to main. Some projects let CMake find their sources with a glob, instead of listing them. CMake's documentation advises against it, as the build doesn't notice a new file until CMake runs again, so we list ours.
15:23 Let's build it again, and this time, Ninja compiles window dot c c, and the client links, with all three functions found.
15:32 When we run it, we get an empty window, cleared to that dark blue, and that's what we expect at this point. Sixty times a second, our loop clears a frame, and raylib shows it. Next, we're going to put something in it, with a library for user interfaces.
Dear ImGui
15:51 For the user interface, we're going to use Dear ImGui. It's a C++ library for building user interfaces inside games and tools, by Omar Cornut, and it doesn't depend on anything else. It's called immediate mode, as we don't create a button and keep it around. Instead, each frame, we call a function for each widget, such as a button, which draws it and tells us right away whether it was clicked. That makes it quick to add a menu or a debug window, as the UI is plain code. A lot of games and engines use it for their tools and debug windows, and that's what we're going to use it for as well. It comes with backends for a lot of platforms and graphics APIs, which connect it to the input and the drawing. raylib isn't one of them, so we need a second library to connect the two.
16:37 rlImGui is by Jeffery Myers, and it's part of raylib extras, a set of libraries around raylib. It's a backend for Dear ImGui that reads the input from raylib, and draws the UI with raylib. Using it takes four calls, starting with setup, before the game loop. Then begin and end go around our ImGui calls in each frame, between raylib's begin drawing and end drawing. And shutdown goes after the loop is over, before the window closes. It also comes with Font Awesome, a font of icons, and turns it on by default. Dear ImGui draws text, and has no icons of its own. To get them, we'd download an icon font, along with a header that names each icon, and merge that font into ImGui's when it loads its fonts. rlImGui does that for us as it sets up, so any label can start with an icon, through a macro from its header.
17:32 Back in the top-level CMakeLists, we fetch two more libraries, next to raylib. First, Dear ImGui is pinned to a release, like raylib, here one point nine two point nine b. Then rlImGui, the backend that connects ImGui to raylib, which doesn't make a release for each change. So its URL points at a commit instead, and that long string of letters and numbers is the commit's hash. A commit can't change once it's made, so we get that code whenever we build. Make available now takes all three, raylib, ImGui and rlImGui, and downloads them in one go. Next, neither ImGui nor rlImGui comes with a CMake build we can use. ImGui is meant to be dropped into a project as source files, so we build a library of our own from them, called imgui. Its sources start with ImGui's core, in imgui dot cpp, and then the demo, which is the window we're going to look at in a moment. Then the code for drawing, for tables and for widgets. And rlImGui dot cpp, the backend, which goes into that library as well. Target include directories adds the two libraries' folders, so we can include their headers. They're public, so anything that links imgui gets them too. It links with raylib, which the backend calls into for the input and the drawing. And dash w switches off the compiler's warnings for these sources, as their warnings aren't ours to fix. System, which we gave raylib, wouldn't help here, as it covers a library's headers. These are sources we compile in a target of our own. It's private, so it applies to their code and not to ours. In the client's CMakeLists, the code moves into a library, nanommo client core, like the server's core library in episode one. It's built from the window, and the gui we're going to write next. Target include directories makes the project's root public, so anything that links the library can include our headers by their path. It links raylib and imgui publicly as well, so they come along with it. Abseil's three libraries move here too, and they're public as well, as our window's header uses status or. And the executable now builds from main alone, with a private link to the core library. That way, the tests can link the library later on, without a second main getting in the way. Now the gui header, as ImGui draws through raylib's OpenGL context, so it needs setting up once the window is open. And it needs shutting down before the window closes, so it gets a class of its own, next to the window's. After the header guard comes the Gui class, which works like our window. Its constructor sets ImGui up, and its destructor shuts it down, so we can't forget either one. Setting ImGui up doesn't report an error, so a plain constructor does the job here, without a factory. Copying is deleted again, as two copies would shut ImGui down twice. Then begin, which starts this frame's UI, and end, with our ImGui calls going between the two. End then draws the result over the rest of the frame. This header doesn't include ImGui at all, so the code that uses the Gui doesn't need to know what's behind it. In the source file, we include imgui dot h, ImGui's main header, for its own calls. Then rlImGui dot h, the backend's header, for setting it up. In the constructor, we start with the font, as rlImGui loads ImGui's fonts as it sets up. Set load fonts callback lets us choose which ones, before that happens. A callback is a function we hand to a library, for it to call later. Ours is a lambda, which is a small function written in place, where it's needed. Its square brackets are the capture list, which says what from the code around it the lambda can use. Ours is empty, as it uses nothing around it, and that lets it turn into a plain function pointer. A plain function pointer is what rlImGui takes, as its interface is C-style. Inside, we start with a font config, ImGui's struct for how to load a font. Its size in pixels is thirteen, which is ImGui's default size. We set it anyway, as rlImGui merges Font Awesome into this font once it's loaded. If the size is left unset, ImGui's assertions stop the client as the icons merge in. The comment says why the size is there: rlImGui merges its icons in, which needs a font with its size set. Pixel snap puts each letter on a whole pixel. ImGui turns it on for this font when we pass no config, so with a config of our own, we turn it on as well. Then we add ImGui's default vector font, with our config. It stays sharp at any size, as it's drawn from outlines, not from a bitmap of a fixed size. Above the call, we write that raylib already scales for high-DPI screens, so the font needs no scaling. If we scaled it as well, the text would come out twice as big on a Retina screen. Then rlImGui setup sets ImGui up, with true for its dark theme. ImGui saves the position and size of its windows to a file called imgui dot ini, in the folder the client runs from. We set that file's name to a null pointer, which turns it off. We don't want files appearing wherever the client runs, at least for now. The destructor calls rlImGui shutdown, and begin and end call rlImGui's begin and end. Begin is also where rlImGui hands ImGui the mouse and the keyboard from raylib, for this frame. In main, we include imgui dot h, for the demo window, next to raylib's header, and further down, our gui header. We create the Gui right after the window, so the window is open by the time ImGui sets up. C++ destroys local objects in the reverse order, so ImGui also shuts down before the window closes. Each frame, after clearing the background, we begin the UI, show ImGui's demo window, and end it. All of that goes before end drawing, so the UI is drawn on top of the frame, before it's shown.
23:47 The demo window shows off nearly everything ImGui can do, and the code for each part of it is in the demo's source. Under widgets, the basic ones are buttons, checkboxes, radio buttons, text inputs, sliders and so on. Windows can be resized from the corner, and moved by their title bar. As raylib draws all of it, the UI is part of our frame, at our frame rate, and it's sharp on a high-DPI screen.
Our own menu
24:13 Now, let's make a UI of our own, with a menu bar and a settings window that changes the background. It's going to be a class of its own, which main calls into each frame, so it gets a header and a source file, next to the window's. After the header guard, the menu header includes array, the standard library's array of a fixed size, as the background colour is going to be three floats: red, green and blue. Then the window's header, as build takes the window, so that the menu can close it. The guide asks for the header, and not a forward declaration, which tells the compiler no more than that a class called Window exists, and hides the dependency from the build. The Menu class has build, which we call each frame. ImGui keeps no widgets between frames, so the menus and windows are built each frame, from our own state. Build takes the window, so that the menu can close it, and the background colour. Both are references, like the window's title, but without const this time, so the menu works on the caller's own objects. It changes the colour in place, and not a copy of it. Privately, the menu keeps track of which of its windows are open. The settings window starts closed, and ImGui's demo starts open, like it was in the last step. The underscore at the end of their names is how the style guide marks data members, so they stand out from local variables. The source file includes its header, and imgui dot h, as this is where we call ImGui. Then cstddef, for size t, the standard type for sizes and indices, which our loop counts with, while the window's header comes with the menu's. Then icons font awesome six dot h, from rlImGui's extras folder, which gives each icon in the free set a macro of its own. Then comes an unnamed namespace, like in the server's tests, for what's private to this file. Inside goes a struct for a preset, and a struct is a class whose members are public by default. The style guide uses structs for plain data like this, with nothing to keep private. A preset has a name, as a C string, which is what ImGui takes for its labels, and a colour, as three floats. Then k presets, a table of four of them, so the background can be changed with a single click. It's constexpr, which we saw in episode one, so the compiler builds the whole table while it compiles. The double braces are there because a standard array wraps a plain C array. So the outer braces are for the array object, and the inner ones for what's in it. The four are night, which is our default, then dusk, forest and ember. Their values go from zero to one, as that's how ImGui works with colours. Now we write build, which main calls each frame, between the Gui's begin and end. Begin main menu bar adds a bar along the top of the screen. Like each of ImGui's menus, it returns true when there's something to fill in, and then we fill it in and end it. The bar has a game menu, with quit, which closes the window through a close function we're going to add in a moment. Its label starts with icon F A power off, the power button's macro from that header. The macro is the icon's character, as a string in UTF-8, and C++ joins two string literals that sit side by side into one. So the label is the icon, a space and quit, with no code to put them together. Then a view menu, where each menu item turns a window on and off. Settings gets a gear for its icon, and the demo gets shapes, for the widgets it shows off. We pass the address of our flag, so ImGui flips it when the item is clicked, and shows a tick while it's on. The null pointer in the middle is for a keyboard shortcut to show next to the item, which we don't have. If the settings window is open, we begin it, with the flag again. That gives the window a close button, which clears the flag. Always auto resize makes the window fit what's in it, so we don't have to pick a size. Colour edit three edits the background's three floats in place. We hand it data, a pointer to the floats inside our array. It shows them as numbers, along with a colour square that opens a picker. Then there's a button for each preset, and from the second button on, we stay on the line. So the buttons sit side by side, instead of one under another. Clicking a button copies its colour into the background, and end then finishes the settings window. A window's end works differently from a menu's, as ImGui wants it called after each begin, whatever begin returned. Last, the demo window gets a flag too, which gives it a close button of its own. Each widget function returns true when it's used, so the code that reacts to a button sits right next to it, with no callbacks to wire up. Now the window needs that close function, for the menu's quit to call. raylib's window should close tells us when the user clicks the close button, but there's no call to ask for that from our own code. So our window keeps a flag of its own, closing, which starts out false. It's private, so the menu sets it through close, and nothing else can change it. In the source file, should close is now true when our flag is set, or when raylib says the user has closed the window. And close sets the flag, so quit doesn't close anything right away. The loop sees the flag when it checks should close, before the next frame, and ends cleanly. Then our destructors shut ImGui down and close the window, in that order. The client's CMakeLists adds the menu to the core library. In main, imgui dot h goes, as main no longer calls ImGui directly. Array comes in, for the background, along with the menu's header. We create the menu next to the Gui, and it keeps its open flags from one frame to the next. The background is now a variable, an array of three floats, and it starts at night, the dark blue we had before. Clear background takes raylib's colour type, with bytes, so colour from normalised turns our three floats, with an alpha of one, into one. Normalised here means that each value goes from zero to one, not from zero to two fifty five. Then, where the demo window used to be, we build the menu each frame, with the window and the background. Our window is a unique pointer, and build takes a reference to a window. So the star hands it the window that the pointer owns.
30:41 Let's try it, starting with the view menu, which lists our two windows, each with its icon. Settings opens with the background's colour, which we can now change to dusk, forest, or ember, each with a single click. Clicking the colour square opens ImGui's colour picker, where we can pick any colour we like. As we drag the hue, the background follows it, as each frame is cleared to whatever the colour is at that moment. Clicking anywhere else closes the picker, and from the view menu, we can hide the demo window too.
Rendering into a texture
31:10 Like with the server, I want a test for what we draw. However, a test can't look at a window, and main draws directly into one. So first, we move the drawing out of main, into a renderer. It's going to draw into the window, or into a texture that a test can read back. It gets a header and a source file of its own, like the menu. After the header guard, the renderer header includes array, for the background. The Renderer class draws the game's frames, and draw draws one frame into whatever raylib is drawing to at that moment. That's the window, between begin drawing and end drawing, or a texture, as we're going to see, and the renderer doesn't need to know which. Draw is const, as drawing a frame doesn't change the renderer. Next, the background, which is the colour each frame starts from. Google's style guide makes a class's data members private, so that the class decides how they change. So the colour gets accessors, named the way protobuf names them. Background, in lower case, hands the colour back by const reference, so a caller can read it without a copy, but can't change it. And the function is const too, like draw, as reading the colour doesn't change the renderer. Set background takes a new colour, by const reference, as an array of three floats. Its body copies it over the old background, and that's what our tests are going to call. Mutable background returns a pointer to the colour, and mutable is what protobuf calls an accessor that lets the caller change a field. Its comment says who it's for: the settings window, which edits the colour in place. ImGui's colour picker writes directly into the three floats, so the menu needs to reach them. Last, under private, the member, background, which moves here from main, and starts at our dark blue. The accessors are a line or two each, so their bodies go right in the class, which the style guide allows for very short functions. The source file starts with its own header, and then raylib dot h, as this is where the drawing happens now. Draw clears the background, with the code main had before, and for now, that's all a frame is. Keeping it in the renderer means main and the tests draw frames through one function. So what the test checks is the code the game runs. Next, the offscreen header, for drawing where nobody sees it. It includes raylib dot h, for its render texture type, which our class keeps as a member. Then ostream, the standard output stream, so that we can print a pixel. Then the renderer's header, as render takes a renderer, like the menu's build takes the window. Next, a pixel struct: red, green, blue and alpha, as ints from zero to two fifty five. Each one starts at zero, so a new pixel doesn't start out with garbage in it. Its equals operator is set to equals default, like the window's constructor. Since C++20, the compiler can write one that compares the four members one by one, so a test can compare whole pixels. Then an output operator, the double angle brackets, which prints a pixel. When a check fails, GoogleTest prints the two values it compared, and this is how it prints ours. The Offscreen class owns a texture on the GPU, which we render frames into instead of the window. Its constructor takes a width and a height, and the destructor frees the texture. Copying is deleted again, as two copies would both free it. Render renders a frame from a renderer into the texture. It takes the renderer by const reference, as all it does is call draw, which is const. Read pixel copies the texture back from the GPU, and returns the pixel at x and y. And the member is a render texture 2D, raylib's texture that we can draw into. In the source file, render calls the renderer's draw, which comes with our header, along with the rest of the class. The output operator prints the four numbers in brackets, after rgba, the way CSS writes a colour. It returns the stream, so more can be printed after it. The constructor loads a render texture of our size, in its initialiser list, after the colon. That sets the member up as the object is built, instead of assigning to it afterwards. The destructor unloads it, which frees that memory on the GPU again, so each test that makes one leaves nothing behind. Render begins texture mode, which sends raylib's drawing to the texture, instead of the window. Then the renderer draws a frame, without knowing where it goes. And end texture mode sends the drawing back to the window. Read pixel loads an image from the texture, which copies all of its pixels from the GPU's memory into the CPU's. Get image colour then reads the pixel at x and y. We unload the image, as it's ours to free, and return its four channels as a pixel. Copying a whole texture to read a single pixel is slow, but that's fine for a test, which reads a handful of them. The client's CMakeLists adds the renderer and the offscreen sources to the core library. In main, array goes, and the renderer's header comes in. The background variable gives way to a renderer, and each frame, the renderer draws, where we cleared the background before. And the menu now gets the renderer's mutable background, and as build takes a reference, we dereference the pointer with a star. So the settings window edits the renderer's colour, and works like it did.
Testing the renderer
36:38 To test the renderer, we need a window, which is a bit of a catch. raylib sets up OpenGL along with its window, and without that, there's nothing to draw with, even into a texture. A test shouldn't pop windows up on the screen, so our window can now open hidden. For that, we add Visibility, an enum class with two values, shown and hidden. An enum is a type with a fixed set of named values. Enum class keeps those names inside the type, so we write visibility k hidden. The k is there as the style guide names an enum's values like constants. And the values of an enum class don't quietly turn into plain numbers, so a visibility can't be passed where a number is expected, or the other way round. Then we go back up and add a Doxygen comment, so it shows up in the API docs. There, we explain that raylib needs a window even to draw into a texture, so tests hide theirs. Create takes a visibility as its last argument, with shown as its default. A caller can leave out a default argument, so main doesn't change, and the tests ask for hidden. In the source file, create takes the visibility too, and uses it to pick the window's flags. The flags for the window are now a variable, an unsigned int, as raylib keeps its flags as bits in one number. They start with flag window high D P I, like before. When the window should be hidden, we add flag window hidden, with a vertical bar and equals. That's a bitwise or, which switches that flag's bit on, and leaves the others as they are. Then set config flags takes them all at once, before init window opens the window. Now the test, in renderer test dot c c, next to the renderer's own files, like the server's test sits next to its code. It includes the renderer's header first, like in the server's tests. Then memory, for the window's unique pointer, and status or, for what create returns. Then come the offscreen and window headers, for the texture and the window. And gtest dot h, GoogleTest's header, which sorts in with ours by its path. The test is called renderer, clears to the background colour. It pins down that a frame starts from the background colour, which everything else we draw is going to sit on. It creates a hidden window, sixty-four by sixty-four, for raylib's OpenGL. That's small, as we don't need many pixels to check a colour. Assert true then checks that the window opened, like the server's tests do with the tick interval. If it didn't, the test stops there, as nothing after it can work without a window. We keep the status or as it is, as the test doesn't call the window. It needs the window open until the end, and the status or holds it until then. Then an offscreen texture of that size, and a renderer. Set background then gives it a blue, with its red, green and blue written as parts of two fifty five. That's thirty, one forty four and two fifty five, so we know which bytes to expect back. Render then draws a frame into the texture, and we expect the first pixel and the last one to be that blue, fully opaque. Two opposite corners tell us the whole frame was cleared, and not a corner of it. Expect equal compares them with the equals operator the compiler wrote for us. And if they differ, it prints both, through our output operator. In CMake, the client gets its own test executable, nanommo client tests. It links the core library, which is why we split it out in the first place, along with gtest main, like the server's tests. Then include GoogleTest, and gtest discover tests registers its tests with CTest. So one ctest command runs the server's tests and the client's.
40:22 Let's build it, and run the whole suite through CTest, with the server's tests and the client's. On my Mac, all four tests pass, so the hidden window opened for a moment, and the renderer cleared the texture to our blue.
No screen in Docker
40:35 Now let's run the tests in Docker, which is where CI runs them. The toolchain comes from Docker's cache, so it's our own code that gets compiled, this time with GCC on Ubuntu. Then CTest runs the tests, and the client's test fails, while the server's pass. GLFW, which raylib opens its windows with, says the display environment variable is missing. On Linux, a window needs an X server to show it on, and display tells a program which one to use. A container doesn't have one, and neither do GitHub's machines. So init window fails, and is window ready is false. Create returns its unavailable error, and assert true sees that the window isn't OK, so it stops the test there. The test is right to fail, as our code needs a display, and this machine doesn't have one.
41:28 So, let's give it a display, and in the toolchain stage, we install three more packages for that. Xvfb is an X server that keeps its screen in memory, instead of showing it on a monitor. That's a virtual display, and programs can open their windows on it like on a real one. X auth comes with it, for letting programs connect to it. And lib G L one mesa D R I is Mesa's OpenGL drivers. Among them is llvmpipe, which runs OpenGL on the CPU, so we don't need a GPU either. It's slow next to a graphics card, but for a sixty-four by sixty-four texture, it's plenty. Then, down in the test stage, CTest now runs through Xvfb run. It starts a virtual display, points the display variable at it for the command it runs, and stops it once the command is done. Dash dash auto server num picks a free display number, so it doesn't clash with a display that's already running. A comment over it tells whoever reads the Dockerfile why it's there: Xvfb run gives the client's tests a virtual display to open their window on. The tests don't change, and neither does the rest of our code. They open their hidden window, like they do on the Mac, and here, it lands on a display that lives in memory.
42:45 Let's run the Docker build again, now that the toolchain has Xvfb in it. The toolchain comes from the cache, and our code gets compiled and tested. This time, the client's test opens its window on the virtual display, renders on llvmpipe, and passes. The test is going to run like this in CI too, on GitHub's machines, with no changes to the workflow, as CI runs this Docker build.
Drawing a cube
43:11 Now that the client is tested, let's draw our character, which is a cube for now. Before the code, let's look at how it gets to the screen. raylib does most of it for us, but we're going to write two parts of it. Everything the GPU draws is made of triangles, so a cube is twelve of them, two on each of its six faces. For each corner of each triangle, the GPU runs a vertex shader. That's a small program, and its job is to work out where the corner lands on the screen. The rasteriser then works out which pixels each triangle covers, and that part is built into the GPU. For each of those pixels, the GPU runs a fragment shader, which is the second program, and it picks the pixel's colour. And the colour ends up in the image we draw into, which is the window, or our texture in the tests. Shaders are written in GLSL, a language that looks a lot like C. raylib's default shaders colour a cube flat, with all of its faces alike. So to make our cube look solid, we're going to write our own, with a bit of light. With OpenGL, we hand the GLSL to the driver as text, while the program runs, and the driver compiles it for the GPU in the machine.
44:21 The vertex shader needs to know where each corner goes, so let's look at where that comes from. Our cube's corners go from minus a half to a half, around its own middle. To put a cube somewhere in the world, we move it there and scale it to its size, with the model matrix. Then the view matrix moves the whole world so that it's seen from the camera, as if the camera sat at the origin, facing forward. And the projection matrix adds perspective, so that things further away come out smaller. Multiplying the three gives us one matrix for each cube, which takes a corner of the cube all the way to the screen. raylib works them out for us, from a camera and each cube's position and size, and hands the result to the vertex shader.
45:02 First, a new header, cube dot h, for the thing we're going to draw. It's a header with no source file next to it, as there's no code to go with it. After the header guard, it includes raylib dot h, for its vector and colour types. The cube is plain data, so we make it a struct. Its position is where its middle is in the world. That's a vector 3, raylib's type for three floats, x, y and z. Its size along each axis is one by default, so a plain cube. Later on, we're going to flatten it into a tile. And its colour is white by default, which is one of the colours raylib defines for us. The braces and the equals after each member are default member initialisers. Whoever makes a cube can leave out what they don't need, and get these values. Next, a header for the camera, which says where we look at the world from. After its header guard, it includes raylib dot h, for raylib's camera type. It declares one function, game camera, named that way as raylib already has a type called camera. Our functions are named in Pascal case, like types are, so a function called camera would be easy to mix up with it. It returns a camera 3D, which is raylib's description of a camera. raylib builds the view and the projection matrices from it, as we saw in the diagram. A plain function is enough here, as the camera doesn't move yet. The source file includes its header, which brings raylib's types along with it. In our namespace, game camera returns a camera 3D, built with designated initialisers, the C++20 feature from episode one. We name each field as we set it, with a dot before its name. So it's clear which value goes where, and the compiler checks that the names exist, in the order the struct declares them. Position is where the camera sits: eleven units up, and ten units back, towards us. Target is the point it looks at, the middle of the world, at the origin. So the camera looks down at the world at an angle, the way a lot of MMOs do. Up tells it which way is up on the screen, which is along y. Fovy is the vertical field of view, fifty degrees from top to bottom. A wider angle shows more of the world, and stretches it more at the edges. And the projection is perspective, not orthographic, where things would stay one size however far away they are. The client's CMakeLists adds the camera to the core library.
47:30 Now, the renderer, which is going to draw cubes with our own shaders. Its header now includes raylib dot h, as the class is going to hold raylib's shader and model types. Then utility, for std move, as set cubes moves the list it's given into the renderer. And vector, the standard library's growable array, for the list of cubes, and our cube header. The renderer is going to load the cube and its shaders onto the GPU, so it gets a constructor. Like the Gui, it needs the window to be open first, as anything we load onto the GPU goes through raylib's OpenGL context. The destructor unloads them, and copying is deleted, like our other classes that own something. Then set cubes, which hands the renderer the list of cubes to draw each frame, over the background. It takes the vector by value, and moves it into the member with std move. So a caller that's done with its list hands it over without a copy. The renderer draws whatever is in the list, without knowing what each cube stands for. Under private, next to the background, goes cubes, which holds the list. Then the renderer owns a shader, which is our two shaders, compiled and linked into one program. And a model, called cube, which holds the cube's mesh and its material. A mesh is the cube's triangles in GPU memory, and a material says how to draw them. We keep a single cube model, and draw it as many times as we need, moved, scaled and tinted each time. In the source file, raylib dot h goes, as the header includes it now, and the camera's header comes in. Then an unnamed namespace holds our two shaders, each one a constexpr pointer to a raw string literal. A raw string starts with R, a quote and a bracket, and ends with a bracket and a quote. In between, the text is taken as it is, line breaks and all, without any escaping. So the GLSL reads like a file of its own, and it's built into the client, with no shader files to ship next to it. The vertex shader starts with hash version three thirty, which is the version of GLSL we write it in. It goes with OpenGL three point three, the version raylib needs. Its inputs are vertex position and vertex normal, where a normal is the direction a face points in, at right angles to it. raylib fills both in from the mesh, by their names, so the names have to be these. Then come two uniforms, which are values that stay put across a draw, while the inputs change from one corner to the next. raylib sets them for each cube, by name again, like the inputs. M V P is the matrix from the diagram, with the model, view and projection multiplied together. Mat normal turns the cube's normals the way the cube is turned. Out float light is what this shader hands on to the fragment shader. In the shader's main, we turn the normal by mat normal, and normalise it, which makes its length one. The dot product we're about to take needs that, to come out between minus one and one. Sun is the direction the light comes from: from above, a little to the right, and towards us. The dot product of the normal and the sun is one when a face points directly at the sun. It's zero when the face is side-on, and below zero when it faces away. Max keeps it at zero or more, and the light is a base of point five five. On top of that comes up to point four five more, the more a face points at the sun. The base means no face goes black, and the rest gives each face a shade of its own. Last, gl position is where a vertex shader writes its result: the corner, times the M V P. The one at the end turns the corner's three numbers into four, which is what a four by four matrix works with. The fragment shader takes the light in, along with col diffuse, a uniform that raylib sets to the colour we draw with. And its output, pixel, is that colour's red, green and blue, times the light, with its alpha unchanged. The three corners of a face share their normal, so they share their light, and the whole face comes out in one shade. Now the constructor, which compiles our two shaders with load shader from memory, in its initialiser list. Then it loads the cube's model from a mesh, which gen mesh cube generates, one unit on each side. The model comes with raylib's default shader, so we hand ours to its material instead. Then the destructor frees both, and its comment warns that unload model leaves the model's shader alone, as models can share one. So the destructor unloads the model, and then the shader, with a call of its own. Without that second call, the shader would stay on the GPU until the client exits. In draw, after clearing the background, begin mode 3D starts drawing in 3D, seen from the camera that game camera returns. That's where raylib works out the view and projection matrices. For each cube, draw model E X draws our model at the cube's position. The axis and the angle are for turning it, which we don't do yet, so the angle is zero. The size scales it, so one model can be a cube or, later on, a flat tile. And the colour tints it, which is what reaches the shader as col diffuse. The loop takes each cube by const reference, so nothing is copied, and nothing is changed. End mode 3D then goes back to 2D, for the UI that's drawn after it. Back in the window, we add flag M S A A four X hint to its flags. It asks for four samples for each pixel, which is multisample anti-aliasing. The extra samples smooth the jagged edges of the cube, where a pixel is part cube and part background. It's a hint, so a driver that can't do it still opens the window.
53:12 Written out as a formula, each face starts from a base of point five five. The dot product with the sun goes up to one as the face turns towards it, and max cuts off what's below zero. So a face turned away from the sun gets the base and nothing more. And with point four five on top, a face that points at the sun comes out at one, its full colour.
53:34 Now that there's something to look at, ImGui's demo window starts closed, so it doesn't cover the cube. It's still in the view menu, for when we want it. In main, set cubes hands the renderer a list with a single cube in it, in orange. The outer braces are the list, and the inner ones are the cube. The designated initialiser sets its colour and nothing else, so its position and size keep their defaults. That puts it at the origin, one unit on each side. We add it once, before the loop, as it doesn't move yet.
54:08 Let's run the client and see it, before we write a test for it. The cube sits in the middle of the window, in orange. We see its top and its front, and the top is brighter, as it faces the sun, which is what makes it look solid.
54:23 Now that we've seen it, a second test checks that a cube gets drawn, where we expect it. Without it, a mistake in the shaders or the camera could leave the frame empty. The first test would still pass, as the background would still be right. It starts like the first one, where we create a hidden window and check that it opened. Then come an offscreen texture of sixty-four by sixty-four, a renderer, and the blue background. Then set cubes gives it a red cube, at the origin, with the other defaults. We render a frame into the texture, and the camera looks at the origin. So the cube should land in the middle of the image, at thirty-two, thirty-two. That pixel should be red, so its red channel should be above a hundred, and its green and blue should be zero. Expect G T is GoogleTest's greater than, which we use for the red channel. We don't check for one particular red, as the light depends on which face we're looking at. That way, moving the sun a little doesn't break the test, and the check still tells the cube from the background. Last, the corner should still be the background, so we know the cube didn't fill the whole frame.
55:30 Once it's built, let's run the tests, and all five of them pass, so the cube lands in the middle of the texture.
A ground and a moving cube
55:37 Next, let's give the cube some ground to stand on, and then move it. A new header, scene dot h, for what's in the world around us. It includes vector and our cube, and declares ground, which returns the cubes that make up the ground. That's a board of tiles, twelve by twelve, around the origin. Each tile is a flattened cube, so the renderer can draw the ground with what it already has. The source file includes vector again, for the list it builds, as it's a standard header. Then an unnamed namespace holds k tiles, which is twelve. That's enough room to walk around in, and few enough cubes to draw. Then two colours, a light and a dark tile, in greys with a little blue in them. Ground starts with an empty list of cubes, and fills it a row at a time. The outer loop counts the rows along z, and the inner one counts the columns along x. Each tile's centre is its column and row, moved back by half the board, so that the board is centred on the origin. The extra half puts the middle of the tile there, not its corner. Its height is minus a tenth, and the comment over that line says why. A tile is a flat box, a fifth of a unit thick, with its top at zero. That's where the cube is going to stand, and the thickness gives the board an edge when we see it from the side. Then light is true when the column plus the row is even. So the tiles alternate between light and dark, like a chessboard. That's what lets us see the cube move, as on a plain floor, there'd be nothing to move past. We add each tile to the list, a unit wide and deep, and a fifth of a unit thick, in its colour. And at the end, we return the list, with all hundred and forty four tiles in it. We don't have to care about the order we draw the tiles and the cube in. 3D mode uses a depth buffer, which keeps how far away each pixel's colour came from. So whatever is nearer to the camera ends up on top. Then input dot h, for the keyboard, as reading the keys gets a file of its own. That way, main doesn't need to know which key does what. After the header guard, it includes raylib dot h, for vector 3. It declares movement, which returns the way the player wants to move, as a vector. And its Doxygen comment says what the numbers mean: x is to the right and z is towards the camera, each minus one, zero or one. So whoever calls movement knows which way is which, without reading the code behind it. The source file includes its header, which brings raylib's key codes along with it. Then movement starts with down, a lambda again, which tells us whether either of two keys is held. This one takes two arguments, a key and an arrow key. A lambda's type has no name we could write, so we declare down with auto, which takes the type from what it's given. Is key down is raylib's, and it's true in each frame while a key is held, which is what we want here. Is key pressed is true in the frame the key goes down and nowhere else, so with it, the cube would move for a single frame and stop. The direction starts at zero on all three axes, and its y stays at zero, as walking doesn't take us up or down. Then W, or the up arrow, moves away from the camera, so z goes down by one. S, or the down arrow, moves towards it, so z goes up by one. A and D, or left and right, move along x. They're separate ifs, so two keys at once move diagonally, and opposite keys cancel each other out. Then we return the direction, for main to move the cube with. The client's CMakeLists adds the two new sources, scene and input. In main, we include raymath dot h, raylib's maths header. In C++, it gives raylib's vectors the usual operators, so we can add and multiply them like numbers. Then vector, for the list of cubes that main now builds. Then our input and scene headers, for movement and ground. The cube we added before the loop goes, as the list of cubes is going to be rebuilt each frame. Instead, position is where our cube's middle is, and it starts at the origin, half a unit up, so it stands on the ground. Then k speed, which is four, and the comment over it gives the unit: tiles a second. Each frame, before drawing, we move the cube by the direction that movement gives us, after Vector3 normalize scales it to a length of one. Without that, two keys at once would move the cube about forty percent faster, along the diagonal. We multiply it by the speed, and by get frame time, the seconds the last frame took. That way, the cube moves four tiles a second at any frame rate. Then we build the list of cubes, starting from the ground. Push back adds our cube to the end of it, at its position, in orange. And set cubes hands the list to the renderer, with std move, so the cubes move into the renderer instead of being copied. We rebuild the ground each frame, which is a little wasteful, as it doesn't change. It's a hundred and forty four cubes, so it costs very little, and it keeps main simple. For now, the client moves the cube on its own, and nobody else knows about it. That's going to change once there's a server that decides where each player is.
1:00:54 Let's write out how the cube moves each frame, as a formula. Its position goes up by its direction, times the speed, times delta t, which is the seconds that frame took. At thirty frames a second, a frame takes a thirtieth of a second. So in a second, the cube moves thirty times four times a thirtieth, which is four tiles. At a hundred and twenty, each step is four times smaller, and there are four times as many of them, so it's four tiles again. Without delta t, it would move four times as fast at a hundred and twenty frames a second.
1:01:27 Let's try it, and holding D moves the cube to the right. W moves it away from us, and it gets smaller as it goes, which is the perspective at work. And two keys at once move it diagonally, here A and S together. Once it's off to one side, we see that side of it too, in a shade of its own, as the camera looks at it from a little to the side.
1:01:49 The ground looks right in the window, so let's pin that down with a third test, and at the top, it includes the scene's header, for ground. This one checks that the picture is the right way round, with the background above and the ground below. It sets up like the others: a hidden window, an offscreen texture of sixty-four by sixty-four, a renderer and our blue background. We write the set-up out in each test, instead of sharing it in a GoogleTest fixture, so that each test reads on its own, from top to bottom. Then set cubes gives the renderer the ground, on its own, with no cube on it. And we render a frame, where the camera looks down at the ground at an angle. So the board fills the bottom of the image, and the background shows above it. Near the top, two rows down, the pixel should be the background. And three quarters of the way down, at row forty-eight, it should be the ground. We don't know which tile lands there, light or dark. So expect not equal checks that it isn't the background, without naming a colour.
Upside down
1:02:50 Let's build it, and run the client's tests, where the new one fails. Three quarters of the way down, where the ground should be, we got the background's blue.
1:03:02 So, let's go back to the offscreen code and see what happened. Read pixel loads the texture as an image, and reads the pixel at x and y, with y counting rows from the top. However, OpenGL keeps a texture's rows starting from the bottom, so the image comes back upside down. The row we asked for, three quarters of the way down, was really a quarter of the way down, in the background above the ground. Our first two tests couldn't tell, as one cleared the texture to a single colour. The other drew the cube in the middle, which is in the middle either way up. The ground is the first thing we draw that looks different upside down. To fix it, right after loading the image, image flip vertical flips it, before we read from it. So row zero is at the top again, like it is in the window. Then we go back up and add a comment over it, as someone reading this later could take the flip for a mistake, and remove it. It says: OpenGL stores a texture's rows from the bottom up, so we flip the image. Reading the rows upside down is a common mistake in graphics code, and we'd have run into it again the day we save screenshots.
1:04:09 Let's rebuild and run the whole suite through CTest, and all six tests pass, so the ground now comes out the right way up.
Docs
1:04:20 Last, like in episode one, we document what we built in the README, so that anyone who clones the repository can build and run the client without watching this episode. The README lists what the client needs on each platform. On macOS and Windows, raylib uses the system's OpenGL, so there's nothing more to install. On Ubuntu, it's the X11 and OpenGL headers, like in our Dockerfile. It shows how to run the client, and which keys move the cube. And it explains how the client's tests run in Docker, on Xvfb and llvmpipe. And the API docs now cover the client's sources as well, so the classes we wrote today each get a page of their own.
Wrap-up
1:05:03 So, today we opened a window with raylib, through a factory that returns the window or an error. A frame loop clears it, and Dear ImGui draws our own menu on top. A test renders a frame into a texture and checks its pixels. In Docker, it failed at first, as there was no display to open a window on. And now it runs on a virtual one, without a screen or a GPU. Then we wrote our first shaders, and drew a cube. A test for the ground caught our pixels coming back upside down. And the cube moves with the keys, at four tiles a second, whatever the frame rate.
Playing it
1:06:40 Like last time, let me know in the comments what you'd like to see, and which parts you'd like us to go deeper on. And if you want to follow along with the series, subscribe. I hope you find this useful, and thanks for watching.