#SuperUserDone
Blog|

Modular game engines and Abusing premake

Cover Image for Modular game engines and Abusing premake
Fri Dec 01 2023

When I started with Pyro engine, I knew that I would need to create a clean and robust buildsystem without a complicated mess of scripts. Initialy I wanted to build upon CMAKE, but most of my projects up to this point used CMAKE and no matter how hard I tried, it always turns into a mess after a while. So I started looking into alternatives.

I immediately ruled out making my own system as I think it would be a waste of time that I could use to work on my engine. In the end I decided to look at the following systems.

Buildsystem Why I chose to look at it
Keep using CMAKE This was my failsafe if I did not find something I liked more.
Bazel Used by Google for all their stuff
Premake Used by many large studios, very powerfull lua scripting, easy to hack
SCons I knew Godot used it effectively

I did not add Meson to the list as I am scared of its warp package manager and the visual studio generator sucks.

Bazel

Bazel is the build tool for Google and is used for all their stuff. I decided to reject it as I could not get it working with Visual Studio and it needs msys.

SCons

Scons is the buildsystem the Godot Engine uses, and rather effectively I must add. I wated to use a simmilar code layout as Godot, so it seemed like a great choice. In the end I decided that I like the syntax of lua more.

Premake

Premake is used by a large number of AAA game studios, and I think I know why. Initialy I did not like the design, but it grew on me as it is super hackable and has everything I need.

Hacking Premake

The more I use premake the more I believe that it is designed to be hacked into the perfect build system for your needs, so that is what I did. The first thing I did was designing the architecture of my engine and building everything around that.

The architecture I decided on was loosely based on the design of Godot, where the engine is split into many modules that are mostly independant of each other. What I mean about that is modules can depend on eg. the memory system, but physics should not depend on rendering or input. I also decided that each module must be able to compile to either an DLL or an Static library and linked into the engine at the flick of a switch. The reason I decided to do it this way was because it lowers link time if everything can be dlls for debug, and the final build can then be statically linked. Theoretically I could use incremental linking with visual studio, but I ran into issues before with that, so I would rather just use this system.

I wanted to be able to do something like this in the main file.

-- MODULE_DEFS
	include_modules({
    "pyro_memory",
    "pyro_containers",
    "pyro_reflection",
    "pyro_rendering",
    -- ...
	})

  -- RUNTIME_DEFS
	include_runtimes({
		{"launch_win32", "WindowedApp"}
	})

  -- TOOL_DEFS
	include_tools({
		"pyro_asset_tool"

	})

  -- TEST_DEFS
	include_tests({
		"pyro_memory_test",
        -- ...
	})

  -- EXTERNAL_DEFS
	include_externals({
		"gtest",
        -- ...
	})

  -- BENCHMARK_DEFS
  include_benchmarks({
    -- you get the idea
  })

This system uses a template to auto create modules, runtimes, etc if the declared type is not found.

The other thing I wanted was to use a custom premake command to add dependancies to a module, as the default way to do it in premake you need to manualy add the include directory of a dependancy to the target.

What I came up with is the uses command. In the module/pyro_modulename/pemake5.lua file:

pyro_module "pyro_modulename"
    -- ...

    uses {
        "pyro_other_module"
    }
    -- ...

The process for other types will be simmilar.

I also added some utility functions to map file vpath filter arrays to flat file lists but that is just standard boilerplate.

Implimenting the system

Not comes the big challange, actually implementing this system. For the part that includes modules and creates them if they are missing, it is easy.

function include_modules(table)
	group "engine/modules"
	for i, name in pairs(table) do
	    -- Create the module if not found
	    if not os.isfile("engine/modules/"..name.."/premake5.lua") then
            create_module(name)
	    end

	    include("engine/modules/"..name)
	end
	group ""
end

The create_module() function obviously needs an implementation, but that is the gist of it. The hard code is the using declaration, used for both external and internal(other modules)

The way I decided to implement this system is by creating an additional set of fields for each config, under projects, publicincludedirs and publiclinks. Here is an example of this in the gtest script.

pyro_external_project "gtest"
    kind "StaticLib"

    publiclinks {"gtest"}

    publicincludedirs {
        "gtest/googlemock",
        "gtest/googlemock/include",
        "gtest/googletest",
        "gtest/googletest/include"
    }

    includedirs {
        -- Ommited for brevity
    }
    files {
        -- Ommited for brevity
    }
    removefiles {
        -- Ommited for brevity
    }

The system then recursively iterates through all the items declared in the uses declaration for each project, caches the project dependancies and inserts all the dependancies as includedirs and links into the project. Every project is only touched once with this system to speed things up, and it auto detects dependancy cycles.

The pyro_module() function is used in the module premakes, and they auto declare thepublicincludedirs and publiclinks for the project so you can immediately do uses with the name of the module.

There are many other things it also does, like auto generating a documentation folder for each project and some other minor things, but that should be the gist of it all.

The code for this part is a bit of a mess so I am not including it, but any decent programmer should be able to create a simmilar system in an weekend or two. The only hurdle is you should be comfortable reading the premake code as the API to do this is not documented, though it is used internally for everything.

That concludes the buildsystem of pyro engine! As you can see some simple utilities can easily make life much easier. I hope this inspires you to create your own buildsystem with premake. Happy hacking!