For about a year now I've been somewhat quietly working on a new game project. This has been a really fun project based on the idea of the "Backrooms", a strange space where you go when you clip through reality. I fell down the rabbit hole when I saw this video by Kane Pixels, and almost immediately wanted to build a game based on the idea. Right away I had some challenges to tackle. The world in the "Backrooms" is a sprawling maze that seems to go on forever, and generating an infinite maze in Unreal Engine was not a challenge I wanted to tackle. Primarily I wanted to create a rich gameplay experience based on this concept, and making this happen in a procedural world would be a huge undertaking. So I thought outside the box a bit, and realized that if I could create a maze big enough and use world partitioning to slice it up, it could seem infinite to the player. So the first step then was to see what it would take to actually generate a maze in code. It turns out that generating a perfect maze is pretty straight forward with a modified depth-first approach. Once I had a basic maze generator built, the feature set of the tool exploded as I was able to add more and more features to create a richer world.
The problem with the existing tool was that I wrote it in C# with windows forms and used the System Drawing library to draw the maze in 2D. As I started to plan new features for the editor, I began to feel the strain of windows forms controls. Around that time I found an article written by a tools engineer at Bungie about the life of a tools engineer. The article was well written and really insightful, but what really caught my attention was the inclusion of a few screenshots of tools that Bungie had developed for Destiny. One picture in particular (the last one in the article) was a screenshot of a world editor, and I was blown away by how clean and sleek it looked. It was feature packed, but well organized, all the tools needed were present, but it wasn't cluttered. Immediately I wanted to try and restructure my maze generator into a similar format and style. To do this though I knew I would need to switch to WPF to really take control over the visual style of my app. The problem with that is that WPF doesn't support the System Drawing library which I was relying on to display the maze. This was actually a great opportunity to try and learn something new, so instead of trying to find a new way to draw the maze in 2D, I decided I would try and draw the maze in 3D instead. I figured that if the Bungie tools were doing it, then I could do it too! My graphics API of choice was Vulkan, so I got to work doing some research on hosting a Vulkan app in WPF. I instead found an example using OpenGL, which was the catalyst I needed to make the leap to Vulkan. After sorting out all the implementation issues with Vulkan, I finally had a working demo. It was primitive and had nothing to do with generating a maze, but my demo was able to render some test objects onto a HwndHost inside of a WPF app!
Fast forward from my test app, and I have implemented the basic maze generator on the C# side, and can render it flawlessly on the C++ side! I implemented a simple work queue between the two layers to allow the C# controls to modify the state of the C++ code. I'm not sure this will be the best method moving forward, but right now its the simplest way to transfer data without having to sync or wait between the layers. There may be a better method, but for now this works well enough.
One other issue I was facing was getting clean user input to allow camera control. Initially I started by using the WndProc function in the C++ HwndHost derived class. It seemed like the most straight forward way to get user input into the C++ side of things, however I quickly discovered that the event driven nature of WndProc would be problematic. Trying to keep track of the state when input events could come in at any time made it difficult maintain an accurate account of the state. It would mean that for part of the frame the input could be one state, and another state in another part of the frame. It also introduced a bug that would cause the camera to move uncontrollably in the last direction the user moved. To fix this, I switched to a very simple poll method to poll the input state exactly once per frame. The actual states are stored in an unordered_map in a static class, so its very fast to modify or check the state of the keyboard input at any time. Switching to this method made the input so much smoother and much more reliable!
Overall this tool is far from being done, but what I have now is a really cool tech demo that showcases how powerful the .Net framework can be. Its far from complete though, and I have a nice list of features that need to be implemented such as:
- Mouse input
- The ability to click on an item within the viewport to modify it
- Transformation gizmos to make moving objects in the world easier
- Deferred lighting for a better experience
- An object properties tab
Once I get the new tool in a working state and make some progress on the actual game I'll show off some of the engineering that has gone into the game, so keep an eye out for that! In the mean time check out the video below, or some code snippets from the editor code!
The basic code to get a Vulkan instance running in C# WPF:
// C# WPF Side
private void Window_Loaded(object sender, RoutedEventArgs e)
{
host = new VulkanEngine.VKHwndHost();
HWND_Host_Placeholder.Child = (HwndHost)host;
}
// C++ Side
public ref class VKHwndHost : public HwndHost, IKeyboardInputSink
{
private:
HWND m_hWnd;
HINSTANCE m_hInstance;
LPCWSTR m_sWindowName;
LPCWSTR m_sClassName;
HANDLE m_renderThreadHandle;
int m_width;
int m_height;
protected:
inline virtual HandleRef BuildWindowCore(HandleRef hwndParent) override
{
m_hInstance = (HINSTANCE)GetModuleHandle(NULL);
m_sWindowName = L"Vulkan Host";
m_sClassName = L"VKHostClassHwnd";
WNDCLASS wndClass;
// Do all the setup to register the class here
RegisterClass(&wndClass);
m_width = 1000;
m_height = 1000;
DWORD dwStyle = DS_SETFONT | WS_CHILD | WS_BORDER | DS_CONTROL | WS_VISIBLE;
HWND parentHwnd = (HWND)hwndParent.Handle.ToPointer();
m_hWnd = CreateWindowEx(0,
m_sClassName,
m_sWindowName,
dwStyle,
0, // X position of window
0, // Y position of window
m_width, // width of window
m_height, // height of window
parentHwnd,
0, // no menu
m_hInstance,
0);
// Make sure the window was created successfully
if (!m_hWnd)
{
// ...
}
DWORD threadId;
// Create a thread to handle the rendering loop
m_renderThreadHandle = CreateThread(
NULL,
0,
RenderLoop, // This is a global function, not shown here
NULL,
0,
&threadId);
// Handle high DPI scaling
HDC hdc = GetDC(m_hWnd);
m_dpiScaleX = GetDeviceCaps(hdc, LOGPIXELSX) / 96.0;
m_dpiScaleY = GetDeviceCaps(hdc, LOGPIXELSY) / 96.0;
// In the actual app, there is some code here to set up the main camera
// The graphicsCore class is the Vulkan implementation. InitGraphics() handles surface creation and
// all the device initialization to set up Vulkan.
graphicsCore.InitGraphics(m_hInstance, m_hWnd, (int)(m_width*m_dpiScaleX), (int)(m_height*m_dpiScaleY));
return HandleRef(this, IntPtr(m_hWnd));
}
}
A quick look at the input system:
static enum class eKeyState
{
E_KEY_DOWN = 0,
E_KEY_UP = 1,
E_INVALID = 2,
E_MAX_ENUM = 3,
};
static class InputState
{
public:
static void SetKeyState(std::string key_string, eKeyState state);
static void SetKeyDown(std::string key_string);
static void SetKeyUp(std::string key_string);
static eKeyState GetKeyState(std::string key_string);
// Add extra methods to track mouse state and gamepad state here
private:
static std::unordered_map<std::string, eKeyState> states;
};
void PollInputs()
{
// One example of checking the key state for the W key
if (GetAsyncKeyState('W') & 0x8000)
{
InputState::SetKeyDown("W");
}
else
{
InputState::SetKeyUp("W");
}
}