Warlock Engine
Project Status Ongoing
Project Type Professional / Personal
Project Duration Started in July 2016
Software Used Visual Studio..
Languages Used C++, C#

HEv3 (or... Hugo's Engine v3, as it's my third time making an "engine"), is a work-in-progress game engine that aims to be modular, portable and cross-platform.

The engine is designed in such a way that each subsystem is a separate Visual Studio project, with its own set of build rules, which specify what modules it depends on, what libraries it might need to compile, based on the target platform, etc.

The Build System

HEv3's build system completely replaces that of the IDE. In summary, it features the following:

  • Written in C#
  • Creates a dependency graph from the modules, taking into account all build rules
  • Incrementally builds each source file, keeping track of modified files using a custom binary database
  • Multithreaded compilation of source files
  • Uses compiler features to extrapolate include files, if not available uses mcpp
  • Visual Studio project file generation (on XML level)
  • C++ code generation
  • Support for many platforms, at the moment Windows, Android, GameCube, PS3, PS4 and PS Vita
  • Proper support for circular dependencies
  • Preprocessing of "special" source files for class definition macro's (more on this in a bit)

Build Rules Example

The following demonstrates the use of build rules for a module within the engine. This is for a module that controls the debug HUD, currently implementing imgui:

using BuildTool;
using BuildTool.BuildSystem;
using ModuleSystem;

public class DebugHud : BuildRules
{
	public override void Register(BuildRulesConfig Config)
	{
		BuildType = BuildOutputTypes.StaticLib;

		// Import dependency modules here, etc.
		DependencyModules.Add("Base");
		DependencyModules.Add("ServiceRegistry");
		DependencyModules.Add("Renderer");

		bool UseImgui = false;

		if (Config.Platform == TargetPlatformType.Windows)
		{
		     Defines.Add("IMGUI_D3D12");
		     UseImgui = true;
		}

		if (UseImgui)
		{
		    Defines.Add("WITH_IMGUI");
		    DependencyModules.Add("imgui");
		}
	}
}

Engine Design

This is still very much work-in-progress, so please take it with a grain of salt :)

Imagine the following situation:

HEv3.png#asset:140

In this case, the "Job Manager" module depends on both the Kernel module (which could host platform specific functions, like the number of available hardware threads), and the Threading module, which has all the logic for thread specific functionality.

Each module has a unique identifier, which is registered at runtime. The identifier is used to verify if that module (and all of its dependencies) have been loaded and are ready to be used. An interesting question comes up with circular dependencies, for instance in the case of the Kernel and Threading module from my example above. A set of rules apply that allow circular dependencies:

  • A module must never use functions of a dependency module in its constructor
  • Each module must have two static functions: CreateDescriptor() and GetUID(). The former creates a list of properties describing the class, including its name, size, and a list of dependency types.

There are a few more rules, but those two are the most important. By following those rules, all modules can be loaded in a linear fashion, making sure no modules are used during initialization of other modules.

An example of the CreateDescriptor() function:

HxClassDescriptor* HxRenderContext::CreateDescriptor()
{
	HxClassDescriptor* tD = new HxClassDescriptor("RenderContext", GetUID());

        // Add a class type, this will be the default one when the class is created.
        // We could also add multiple types here, then create an instance based on some config option for example
        // If no type is registered, a runtime error will occur informing about a missing implementation - this makes managing different platforms a lot easier
#ifdef PLATFORM_WIN
	tD->AddClassType<RenderContextD3D12Win>();
#endif
        // Add other class types, etc.

        // Only allow an instance of this class to be created when all class types within the Kernel module have been loaded
        // We could also use AddClassDependency if we only need a specific class
        tD->AddModuleDependency<KernelModule>();

	return tD;
}

Each descriptor keeps a list of dependency class UIDs, and in the example above, the UID of ServiceA is retrieved from the static function of that class.

Hx: A Tiny Reflection System

I implemented a tiny reflection system to help with dynamic instance creation of classes. It uses mcpp with some slight modifications in its source code to spit out some info when a certain macro was detected. Declaring a class looks like this:

HX_DECLARE_CLASS(HEV3::Graphics::Renderer::HxRenderContext)
class HxRenderContext : public HxClass
{
public: // ctor / dtor

	HxRenderContext();
	virtual ~HxRenderContext();

public: // hx functions

	static HxClassDescriptor* CreateDescriptor();

	static HxUid GetUID();
};

From this it parses the HX_DECLARE_CLASS macro, caches the data and later in the pre-process section of the build it generates the module definition:

class RendererModule : public HxModule
{
public: // ctor / dtor
    
    RendererModule();
    virtual ~RendererModule();
    
public: // class types
    
    enum
    {
        HXT_RENDERER = 0xA9A00000,
        HXT_RENDERER_HxRenderContext = 0xA9A046C9, // @ HxRenderContext.h:21
        HXT_RENDERER_TOTAL = 1
    };
    
    HxClassDescriptor* mClassDescriptorList[HXT_RENDERER_TOTAL];
};
RendererModule::RendererModule()
    : HxModule("Renderer", HXT_RENDERER, mClassDescriptorList, HXT_RENDERER_TOTAL)
{
    AddClassDescriptor(HEV3::Graphics::Renderer::HxRenderContext::CreateDescriptor());
}

HEv3_ReflectionWhite.png#asset:258