A lighthouse in the storm

Time marches on relentlessly and I haven’t been very active in blogging recently, so I figured it would be a good time to elaborate a little on my big picture thoughts about Doomsday.

The current situation is that I’m again the only active developer as DaniJ bowed out a couple of years ago. I’m also trying to balance my time between hobbies such as Doomsday, being father to a new baby, and my day job. As you might imagine, this makes it impossible to have a stable time commitment to the project. Fortunately, the Autobuilder is still a solid mechanism to keep things rolling along whenever I find the time to write or debug some code, even when there are hiatuses along the way.

Programming a large project (or any project really) intermittently is a challenge. It takes a certain amount of effort to get immersed and familiar with the code to be productive and to avoid introducing a host of new bugs while making changes. Sometimes it feels like you’re swimming on the ocean, and the gigantic waves obscure your goal except for brief moments when you can see clearly where you’re going. Then the wave takes you down again and distractions cloud your thoughts. Therefore, it is worthwhile to put the long view in writing to make it easier to remember what to do when the opportunity arises — a lighthouse, if you will, to guide one through the choppy waters.

These are the major objectives that I’m pursuing:

  • Redesigned renderer — an independent module.
  • Completely isolating the game: running the game logic in as unmodified form as possible; in its own thread, too.
  • Scripting integration (including game objects). A common scripting API that can be bound to different game backends.

Let’s take a look at these objectives in a bit more detail.

Rendering

Graphics are the #1 element of modern games, and things continue to evolve fast in this area. Nowadays it makes the most sense to target Vulkan as the graphics API, for the widest portability, future-proofing, and performance. (A further wrinkle is Metal on the Mac/iOS, but fortunately there are acceptable workarounds.) However, Vulkan is not exactly a fun and easy API to work with, so there is an argument to make about not using it until the renderer has been fully developed. Shaders can in any case be written in, say, GLSL, which makes development a lot easier to manage — all the heavy lifting in the renderer is done in shaders anyway.

But what exactly should the renderer aim to do? I’ve covered the major parts in the earlier posts. In general, the goal is to create an aesthetic that fits the classic DOOM 2.5D maps, where lighting is mostly ambient and soft, and sharp point lights are reserved for special effects and specific decorative objects such as lanterns and torches. This requires developing a global illumination system, although thanks to the simple map geometry it will be easier to handle than general-purpose 3D GI.

A crucial element are surface materials. In addition to having PBR texture maps, I believe it is mandatory to increase the general detail level of map geometry with a combination of displacement maps and custom 3D model decorations. The displacement maps could in practice be applied via mesh subdivision (both for performance and because the original geometry is so low-detail), while additional 3D models are trivial to instance wherever needed. The hard part is the manual work needed to set up procedural generation rules that work well for the myriad maps out there — both vanilla and mods.

Finally, when it comes to software architecture, there are advantages to keeping the renderer isolated from the game. This ensures there is no hidden coupling between the renderer and the game code. Consequently, the game code can be anything as long as it’s made to interface appropriately with the renderer — facilitating addition of new game modules for specific compatibility modes. In my Doomsday 3 work branch, the renderer is already a separate library, and all data (geometry, textures) is passed to it as a .pack package. Dynamic updating of entities is still missing, and that will require a specifically designed runtime API so the game can tell the renderer where entities are, and how they’re moving and changing state.

Isolated game modules

One of the thorniest issues with Doomsday game plugins is compatibility with vanilla and mod behavior. Over the years, the code has gone through multiple revisions, which has affected gameplay behavior in subtle (and some really obvious) ways.1 Instead of trying to refine and extend the J-ports to work with Boom etc. features, it makes more sense to take the Boom source code and adapt it to work with Doomsday’s renderer instead. This sidesteps all the gameplay compatibility issues, leaving only visualization of special new features as the problem to focus on.

The key to preserving intended behavior is to run the code as it was originally run — unmodified. In effect, what you are after is a virtual machine: the code runs in an environment that matches the original, while there are external video, audio, and input systems. Running the game in a thread of its own completes the isolation in that it is independent from any processing delays occurring in the UI/render thread, and the game can use its own ideal refresh rate.

The J-ports do not operate in this way — everything is muddled together, with Doom, Heretic, and Hexen intermingling within complex #ifdef labyrinths, and visual details such as movement smoothing pervading game objects throughout. A likely future for the J-ports is to fall back to be a headless implementation, discarding the old visual/rendering code completely. Integration with the new renderer is then needed to draw the game world.

Scripting integration

When everything is a separate module, how exactly do the modules interact with each other? This is where Doomsday Script enters the picture. It provides a common data model and command interpreter.

For example, game modules can register a set of script bindings for the various objects they provide: config variables, players, maps, things, and so on. Doomsday can then access these to implement its own menu UIs, game state manipulation, and gameplay scripting. This mechanism is already being used in the new 3D model renderer in Doomsday 2, where model assets can contain scripted behavior, and shader variables and materials can be accessed via scripts.

Scripting acts as the glue that allows the engine to deal with different game modules in a uniform way. As long as the bindings exist, the same script can run in Doom, Heretic, or Hexen, for example.

In current Doomsday, there are aspects that already rely on this kind of thinking. For example, the InFine/finale scripting language used for intermission and game finale animations is game-agnostic. However, the implementation is incoherent, being mostly a collection of ad hoc solutions introduced over time. With Doomsday Script as the underlying foundation, all these can be implemented with less effort and more consistency.

Conclusion

These three objectives should provide an outline of the direction I want Doomsday to be heading: more modularity, and more reliance on already-established game code. This allows focusing Doomsday on its own core strengths such as beautiful graphics and powerful scripting.

Networking falls between these goals somewhat. Isolating the game is a perfect fit for a dedicated server, since the game anyway relies on a separate module for visualization — a server just lacks the rendering module. However, zero-latency playsim on client-side is also important, which means that networking becomes the task of synchronizing world state between multiple independently running game modules. This is largely similar to what Doomsday has been doing so far, but having rendering fully isolated introduces some additional constraints and benefits. The key here could be the script bindings: if they provide complete enough access to game objects, Doomsday can synchronize game states using the bindings only, and the game module does not need to be deeply aware of the presence of the sync mechanism — after all, the goal is to modify game code as little as possible.

What will happen in practice, though? Will it take another 20 years to accomplish these goals? As I mentioned in the beginning, time is currently something that I don’t have much of, but I find this vision compelling enough to keep things rolling along, sporadically as it may be for the time being.

PS. Version 2.2.2 is out.


  1. This applies equally well to Doomsday as other source ports.