I have been working on a build system so that I can build code and assets the way that I want to. A dedicated post about it is here.
It is possible to define different platforms and configurations (e.g. debug or optimized)
It is possible to define three kinds of tasks (C++, copy files, or arbitrary Lua functions)
The task dependencies are tracked, and they are only executed if necessary and in the correct order (only single-threaded, although it has been designed for multi-threaded execution)
For C++:
The installed Visual Studios and Windows SDKs are calculated, and the user can either request the latest by version number or request specific versions if desired
Standard library and platform library support can be enabled or disabled (and, when enabled, the appropriate #include paths are set)
It is possible to build console applications, windowed applications, shared libraries, or static libraries
Exceptions can be enabled or disabled
Next steps
It has now been two weeks that I have been working on the build system, and even though I have made good progress it feels like I really need to start working on actual game engine code. There are still obvious missing features in the build system but I think a good strategy now is to add them as they become necessary for the actual code I am working on rather than trying to add more preemptively.
One final thing that I think that I want to add is the ability to generate a Visual Studio solution that I can use to debug
I think it’s ok initially if I don’t create individual projects for named tasks and instead just have a single project to build the entire solution (it seems ok at least initially to manually find files and open them in the editor)
I do, however, want to be able to debug, and having some intellisense would also be nice, and so I might spend some time trying to get that done.
Another thing that I know I will want is to be able to set up precompiled header files
Being able to do this in a platform-independent way would be nice so that I don’t have to go back and redo it later, but it’s not critical
I may or may not add this before moving on to start work on code
The initial goal for a program would be to open a window that doesn’t do anything, and that is pretty achievable
The next goal would be to clear the color buffer to some hard-coded color
This would be quite easy using Direct3D 11, but using Direct3D 12 requires allocating some memory which will require some memory management work with the way I want to do things (rather than just using global new).
In every job I have had I have become involved with the system of how the software is built, and this is also something I spent time making my graduate students work on in their game engine class. Every time, and in all of my personal projects, I have wished that I had my own system that worked the way that I wanted it to, and while I currently have the time to create a custom game engine I have taken the opportunity to try and make such a build system.
The term “build system” may mean different things to different people, but my usage is to refer to the process of taking any source files (code and assets) and transforming them into the final generated files that can be distributed in order to run the software. In some cases the process can be quite simple conceptually (in order to create an executable application using C++, for example, the source files are compiled into object files and then those object files are linked into an executable), but when creating a game the process becomes much more complex: Not only is there traditional code to deal with but there are also assets (input authored data that is not code) as well as interdependencies between code and assets where the content of one can influence how the other is built.
Wishlist
The following is a non-exhaustive list of what I would want in a build system, which contains elements both of the build system tool and of the implementation of how a piece of software uses a build system:
The build system is simple to use
There should be exactly one step required to go from only the authored files (the ones contained in source control) to the final product that is ready to be distributed
(If there is more than one step that should be the fault of the human and not the build system, and the build system should make it possible to automate any extra steps so that there is only one)
Builds are deterministic
Building the software should always have the same results
If an attempt to build has an error and then a subsequent attempt to build succeeds this is a problem
If an attempt to build has an error there is never a question of whether it is a real problem or not
Nothing is done if a build is requested and nothing needs to be done
It is fast to request a build when nothing needs to be done
When I am developing the common case should be to request the entire software to be built (it is exceptional when I want to build some parts but explicitly don’t want other parts to change) and it should be fast for a “no-op” build so that it doesn’t interfere with iteration
Dependencies are easy to work with
It is easy for a human to specify important dependencies
As much as possible, however, the system should figure out the dependencies itself without requiring a human to specify them
If an attempt to build has an error there is never a question of whether it is because a dependency was missing
When an attempt to build something is made there is never a question of whether the result might not be valid because one of its dependencies was not in a correct state (this is restating the deterministic requirement)
Assets (i.e. things that aren’t code) are easy to build
Generated code is easy to build
It is easy to build abstractions and to document and comment
When I am creating the system that builds software I want to be able to use good programming practices
When I am developing software with a build system that behaves as I describe above then I have the confidence to iterate and make changes and know that I am seeing the correct results of the changes I made, without worrying about extra steps or that something might be wrong.
Philosophy
I had to come up with some name and I settled on “jpmake”, a silly variation on the common naming scheme of “make”, “nmake”, “cmake”, etc., for build system software. It is based on some of the principles I have thought about for years, although its development is focused on allowing me to work on the custom engine project and so there are features that I know that I would like that I will intentionally not work on until/unless they are needed.
Despite using the term “build” and even though the motivation behind creating this is in order to build software I am designing things with the mental model that there is a “project” which is a collection of tasks that must be done (and these tasks may or may not have dependencies on each other). This means conceptually that a jpmake project could be used the same way e.g. a BAT file in Windows often is, to automate a series of tasks. Although a requirement is to be able to easily specify and build C++ programs the design goal is to have that be just one kind of task that can be done, with the hope that approaching it this way will make it easy to add other as-yet unanticipated kinds of tasks. The user should be able to focus on defining which tasks must be done and what the inputs and outputs of each task are, and then jpmake should be able to figure out the rest.
Lua
I am a big fan of the Lua scripting language and use it when I want to create a text-based interface. I have in fact used it multiple times in the past for smaller build systems, focusing on being able to build assets as part of a larger code-based project, and so I already have some experience with what I am trying to currently accomplish with jpmake.
My favorite thing about Lua is the ability to create interfaces that are easy to understand and use. It has enough flexibility that I can create files that are understandable even by someone who doesn’t know Lua (often understandable enough to make small changes), but it also has the power of a full programming language behind it so that I don’t feel limited when I want to use what I consider good programming practices.
As an example, observe the following lines from a jpmake project file:
Without knowing any details you could probably guess what line 2 does, and even without knowing the correct syntax you could probably copy line 2, modify it, and get the correct result that you wanted and expected.
One issue I have run into with previous large-scale Lua projects that I’ve done, however, is that although I am completely satisfied with using the finished product in terms of the interface it has been very difficult to remember the details of the inner workings and to debug when things go wrong. For this current project I am taking a different approach to try and mitigate this, where I use Lua script files as the project files that specify what tasks to execute and how, but otherwise everything is implemented in C++. In the case of how I anticipate using jpmake this is probably what I would have done anyway because it means that there is a single executable application that can do everything, but it also has the advantage of easier maintainability because of static typing and making it easy to debug. (Additionally, of course, it can be more efficient. I am trying to manage all of the strings that exist in a program like this (i.e. because there are so many file paths) in an efficient way to keep things fast that wouldn’t be possible if the implementation were in pure Lua.)
Example
Details will have to wait for a part 2, but below is an example from the test project file that I have been using while developing:
-- Set Up--=======-- Define the platforms that tasks can be executed for forlocalplatform_windows = DefinePlatform("Windows", "win64")-- Get the currently-specified (from the command arguments) platformlocalplatform_current = GetTargetPlatform()localplatformName_current = platform_current:GetName()-- Define the configurations that can be used when executing taskslocalconfiguration_debug = "Debug"localconfiguration_optimized = "Optimized"DefineConfigurations{configuration_debug, configuration_optimized}-- Get the currently-specified (from the command arguments) configurationlocalconfiguration_current = GetConfiguration()-- Define environment variables that can be used in pathsSetEnvironmentVariable("TempDir", table.concat{"temp/", platformName_current, "/", configuration_current, "/"})SetEnvironmentVariable("IntermediateDir", "$(TempDir)intermediate/")SetEnvironmentVariable("OutputDir", "$(TempDir)output/")-- Define the C++ info that determines how C++ is builtlocalcppInfo_common = CreateCppInfo()do-- Libraries can be disabled for situations that only use custom codedocppInfo_common.shouldStandardLibrariesBeAvailable = falsecppInfo_common.shouldPlatformLibrariesBeAvailable = falseendcppInfo_common.shouldExceptionsBeEnabled = false-- Platform-specific configurationif (platform_current == platform_windows) then-- A #define can be set for platform-specific conditional compilationcppInfo_common:AddPreprocessorDefine("PLATFORM_WINDOWS")-- Preprocessor symbols can also have valuescppInfo_common:AddPreprocessorDefine("PLATFORM_NAME", platformName_current)-- Use different VC run-times based on the current configurationcppInfo_common.VisualStudio.shouldCRunTimeBeDebug = configuration_current == configuration_debugendend-- Alternate C++ infos can be created from the base configuration and then have targeted changeslocalcppInfo_withStandardLibraries = cppInfo_common:CreateCopy()docppInfo_withStandardLibraries.shouldStandardLibrariesBeAvailable = trueendlocalcppInfo_withPlatformLibraries = cppInfo_common:CreateCopy()docppInfo_withPlatformLibraries.shouldPlatformLibrariesBeAvailable = trueend-- C++ Tasks--==========do-- A "named task" is just for human organization:-- It allows only a subset of sub tasks to be executed (rather than the entire project) by specifying a namelocalcppNamedTask = CreateNamedTask("BuildMyCppProgram")localstaticLibrary_usingPlatformLibraries = cppNamedTask:BuildCpp{targetType = "staticLibrary", target = "$(IntermediateDir)usesPlatformLibraries.lib",compile = {"MyClass.cpp", },-- Any libraries specified as needing to be linked when defining static library tasks-- don't actually influence the creation of the static libraries themselves,-- but instead are stored as dependencies-- that eventually are linked when an application is created that uses the static librarieslink = {platform_current == platform_windows and CalculateAbsolutePathOfPlatformCppLibrary("Advapi32.lib", cppInfo_withPlatformLibraries) or nil, },-- This static library looks up a registry value,-- and by requesting that platform libraries are available-- it allows the #include <windows.h> directive to look in the correct Windows SDK folderinfo = cppInfo_withPlatformLibraries, }localstaticLibrary_usingStandardLibraries = cppNamedTask:BuildCpp{targetType = "staticLibrary", target = "$(IntermediateDir)usesStandardLibraries.lib",compile = {"MySource.cpp", },-- This static library uses std::cout,-- and by requesting that standard libraries are available-- it allows the #include <iostream> directive to look in the correct Visual Studio folderinfo = cppInfo_withStandardLibraries, }localmyApplication = cppNamedTask:BuildCpp{targetType = "consoleApplication", target = "$(OutputDir)MyApplication.exe",--targetType = "windowedApplication", target = "$(OutputDir)MyApplication.exe",--targetType = "sharedLibrary", target = "$(OutputDir)MyLibrary.dll",compile = {"MyEntryPoint.cpp", },link = {staticLibrary_usingPlatformLibraries,staticLibrary_usingStandardLibraries, },info = cppInfo_common, }end-- Copy Tasks--===========do-- Here is an example of a differently-named task that does something other than building C++localmyTask = CreateNamedTask("myTask")myTask:Copy("someAuthoredFile.txt", "$(OutputDir)someDirectory/someIntermediateFile.txt")myTask:Copy("$(OutputDir)someDirectory/someIntermediateFile.txt", "$(OutputDir)someDirectory/someOtherDirectory/someCopiedFile.txt") end
These posts are intended to be made weekly and are not intended to be of interest to a general audience
Instead, these posts are a way for me to hold myself accountable by reporting what I have worked on during the previous week and what I intend to work on during the upcoming week
I am currently unemployed by choice. I have been very fortunate professionally that I have been able to work on interesting projects, but there are also some things that I have never had a chance to work on that I am personally interested in and would like to explore. Primary among these are:
Graphics
Hardware ray tracing
I fell in love with all things ray tracing while a student at the University of Utah but I have not had an opportunity to develop anything since hardware support was added to consumer GPUs
HDR
Although I understand the concepts of tonemapping I have never had the opportunity to implement it, and after all of the work I have done with color and light it is an obvious next step and gap in my experience that I would like to fill
Memory Management
I have a fascination with manual memory management, and I am interested in attempting to write software that always uses explicit allocators rather than the global new/delete so that all memory is budgeted and uses appropriate allocation strategies
I am also interested in trying to go all-in on “data-oriented design” to gain more experience about how to design large scale software with cache-friendliness and good access patterns as a priority
Engine
I would like a better custom basis that I can use to create applications (not just necessarily games)
I have almost entirely worked with graphics in my professional career and I would like to gain experience and learn the technical aspects of other areas
Game Design
I have ideas of things that I like and things that I don’t like from playing games, but my actual experience of user-facing design is limited to APIs and GUI properties
It can be hard to find the energy or motivation to do personal projects like this with a full-time job, and especially with jobs in the industries that I work in which tend to be high-demand (which, to be fair, is also what makes them fun). With this in mind I have quit my job and am taking a temporary break from employment so that I can focus full time on learning and implementing some of the things listed above.
Goals and Scope
The time frame for working on this project is quite limited before I will have to find another paying job. If the end goal were to actually create a finished game that could be released and sold then the only reasonable strategy would be to use an established game engine (like Unreal or Unity), and even then the only kind of game that a solo developer could create in a short amount of time would have to be extremely limited in scope. The majority of my goals for this project, however, involve creating the technology from scratch. This means that the actual goal is primarily to create a game engine, and any “games” that I make will be more like tech demos.
The meaning of what a game engine is in the popular imagination seems to have changed over the years and my impression is that today many people think of a “game engine” as something like Unreal or Unity where there is an editor and it is meant to be used by external users to make any kind of game imaginable. That definition is not what I mean when I talk about what I want to work on, however: I am interested in making something just for me that serves as a framework to create interactive applications. Although in real game development tools are incredibly important I don’t anticipate (sadly) creating any editors for my current project, and any tools will be programs that build game-ready assets from authored assets.
My ideal outcome would be to actually create some finished experience that I could release in some way, but due to the uncertainty around the time available I don’t believe that that should be my criterion for evaluating whether the project succeeds or fails. Instead, my intent is to try and create several small applications along the way that are unfinished and unpolished (i.e. not releasable to a general audience) but that allow me to have some small scale goal to work towards when implementing features. I will also try to document milestones along the way with posts here. The true measure of success for me personally will be if I have been able to implement some of the features listed in the bullet points above.
With that being said, as a way of setting realistically low expectations my goals for an eventual releasable program would be:
Graphics rendered using hardware ray tracing
Audio sound effects that play dynamically in response to something
A player avatar in some kind of third-person view that can be controlled using an Xbox controller
Some kind of action/interaction that the player character can do with the environment
My graduate students in a semester-long class used to accomplish something similar (without the ray tracing) using a starting engine that I provided and so the list above feels achievable. I will have to decide as time passes whether to try and focus on implementing the above points early and then improving things or whether to be content focusing on the individual features that are interesting in the moment even if it means not finishing a final project.