Cacowards 2018 Announced!

Cacowards 2018

Doomworld’s 2018 Cacowards recently came out and with it a series of amazing articles from Doomworld. You can check them out below

  1. 2018 Cacowards: The quintessential Cacowards, celebrating 25 years of Doom. Also included is an amazing write-up on Erik Alm’s legacy as a mapper and how his enduring vision influenced other legendary projects.
  2. Top 100 Most Memorable Maps: Curated list of the top 100 Doom maps produced by the Doom community. Either as single-map contributions or cherry-picked maps from multi-level WADs.
  3. Top 25 Missed Cacowards: Projects that didn’t win a Cacoward but who looking back, should have been awarded one.

Auferstehung 2 (Doom WAD) Released!

It’s been 12 years since Helldorado Team (formerly Brain Dead Studios, formerly Elioncho’s Engine team, formerly the Condor Team) announced the development of a Doom WAD – the never released Confessions from the Flesh. But today, after more than 10 years of projects sinking down the drain, prototypes never seeing the light of day, and other tech demos lost to numerous formatted hard drives, we can finally announce the release of our first 11-level Doom WAD (wow, it only took a decade): Auferstehung 2.

Auferstehung 2 is the unofficial sequel and homage to Nazi Auferstehung, named the Worst WAD of 2006 by Doomworld.

And now, from the depths of the Doom community a similar turd emerges, albeit not as worse, but still pretty bad. Following the footsteps of the predecessor’s creator (who used Slige), Helldorado Team leveraged Oblige to produce the 11 procedurally generated levels of Auferstehung 2.

Download Auferstehung 2!

Levels:

MAP01: Enter the Dominion
MAP02: The Prison of Despair
MAP03: Horrendous Temple
MAP04: Blood Shrine of the Beast
MAP05: The Nukage Shaft
MAP06: Thermal Terminal
MAP07: Pain from the Base
MAP08: Lockdown
MAP09: The Crossroads of Menace
MAP10: Arson Anthem
MAP11: Palace of Danger

Special thanks to:

  • Andrew Apted for creating Oblige and making this project possible
  • Dark Exodus for Nazi Auferstehung
  • id Software for Doom 2
  • Elioncho for emotional support

Tools used:

How to play:

  1. Drag and drop auferstehung2.wad into the .exe of your favorite Doom Source Port (for example GZDoom, Zandronum, etc). Boom-compatible port recommended.
  2. Make sure Doom2.wad is in the same folder as the Source Port’s .exe file

ARThings v1.2 Released

We made some tweaks over the past couple of weeks and finally ARThings v1.2 is out. Click on the cool Doomguy below to check it out:

Here’s a laundry list of the updates we made:

  1. Updated HUD graphics – Updated fire button graphics and added a new “switch weapon” button
  2. Removed the ability to spawn monsters by tapping on planes. This was an experimental feature pre-v1.2 that has now been removed
  3. Removed the tracking of vertical planes such as walls. This was not needed as monsters spawn on top of upward facing horizontal planes (such as floors).
  4. You can now pick up and use the Chaingun
  5. Updated spawner to spawn Shotgun and Chaingun ammo when you’re low on ammo
  6. Updated spawner to spawn 3 different types of monsters: 1) Zombiemen, 2) Chaingunners and 3) Demons
  7. Dead Zombiemen drop clips when dead
  8. Dead Chaingunners drop Chainguns when dead
  9. Added other effects such as blood and teleport fog when spawning monsters
  10. Other bug fixes

Also, here’s a video of some “in-game” action from v1.2:

Doom AI Game Logic Explained, kinda

A couple of weeks ago we released ARThings, please check it out when you get a chance. Like we mentioned previously this project began as an academic exercise to build something more interactive with ARCore, but to bring this Doom experience to ARCore we had to invest time to learn the inner workings of Doom’s AI/Game Logic system. Below is some light commentary on this experience, including some insights and lessons learned.

Picking a Doom Source Port

To port the Doom AI game logic we had to pick a Doom source port. We narrowed down the list to 2 contenders:

There’s pros and cons with each of these but ultimately we settled with Linux Doom. By doing so we could deal with less complexity. The ZDoom codebase is much more modernized but quite extensive – it not only runs Doom but also Heretic, Hexen, Strife and other “Doom clones”. Learning ZDoom meant going through a much more extensive and technically sophisticated codebase. ZDoom’s AI and gameplay code architecture was revamped to include ACS, a flexible scripting system that developers could use to extend gameplay and AI behavior without having to write additional code to be compiled with the master codebase.

Having settled with Linux Doom, we unzipped the codebase and immediately faced with an even bigger challenge – working with legacy code. Doom was written in the early 90s, and as such the code was meant to push the limits of the hardware of that era (i386, i486, etc). If you get some time, I recommend reading Fabien Sanglard’s Game Engine Black Book, he does an amazing job describing the technical limitations id Software’s faced when developing Wolfenstein 3D. These limitations drove the technical direction of both the Wolfenstein 3D and Doom codebase.

So we went deep into the Doom source code. After studying the source code for a couple of days we encountered these technical pain points:

  1. Reading C – Doom’s code is not Object Oriented. Functions are grouped into source files based on functionality. The code can be hard to come to terms with if you’ve been writing OO code for a while.
  2. Absence of Floating Point arithmetic – To cope with CPU’s lack of floating point arithmetic during the mid-90s Doom uses Fixed point arithmetic. The gist of this is that floating points are represented using 32-bit integers. 16 bits for the integer portion and the remaining 16 for the fractional part.
  3. Angles are represented using Binary Angle Measurement (BAM) – This is new territory if you’re used to storing angles as doubles (or floats) when working with computer graphics or trigonometric calculations. in BAM, angles are represented using the full range of values of a 32-bit unsigned int. The challenge with this is that some languages do not support unsigned int as a primitive data type (Java for instance). You have to provide your own wrapper functions when performing computations that result on unsigned integers. Another challenge with BAM is that to perform trigonometric calculations you have to provide your own special purpose sin/cos/tan lookup tables.
  4. Usage of C function pointers – These are used heavily in the code, sometimes to mimic Polymorphism. C function pointer code can be hard to port with the right architectural approach, especially if the language you are porting to implements similar functionality using a different programming paradigm. Again, using the Java example you can accomplish this using Lambda expressions and Method References.

The Basics – Understanding Mobjs and States

Let’s now get to the basics. To understand how the Doom AI works we need to understand how objects, or Things (mobjs), are represented in the codebase. Things are monsters, items, pickups, fireballs, projectiles, or anything that is represented by a sprite in the game. For example, when you shoot a monster and you see a blood effect rendered as a sprite, the blood effect is a mobj.

Things are represented by the mobj_t struct defined in p_mobj.h. mobjs and have attributes such as (x,y,z) position, (x,y,z) momentum (used to simulate physics) and many other object properties.

Side note: The Doom coordinate system treats Z as the vertical “up” axis, as opposed to the Y axis. If you are coming from the OpenGL world this might be hard to stomach.

For the purposes of AI, a mobj (I’ll be using mobj and Thing interchangeably) functions as a finite state machine. Through its lifespan a mobj will cycle through various states as determined by the core engine AI logic. A mobj’s current state is kept as a reference to a State object:

state_t* state;

The declaration of state_t is defined in info.h and all state definitions in the game are stored as a global array:

typedef struct
{
  spritenum_t	sprite;
  long		frame;
  long		tics;
  // void	(*action) ();
  actionf_t	action;
  statenum_t	nextstate;
  long	        misc1, misc2;
} state_t;

extern state_t	states[NUMSTATES];

The state object has the following attributes and characteristics:

  • sprite – Sprite to use to render the mobj for this state
  • frame – Animation frame to use for this state
  • tics – How many tics to run this state for (We will explain the concept of tics in the next section)
  • action – Pointer to the function to run when transitioning to this state
  • nextstate – State to transition to when this state is over
  • misc1 – Miscellaneous attribute
  • misc2 – Miscellaneous attribute

The list of all state numbers for all the different types of mobjs in the game are defined in the statenum_t enum in info.h.

To decide what states correspond to each mobj type there’s a structure called mobjinfo_t in info.h that contains this information. The engine stores this as a global array as well:

typedef struct
{
    int	doomednum;
    int	spawnstate;
    int	spawnhealth;
    int	seestate;
    int	seesound;
    int	reactiontime;
    int	attacksound;
    int	painstate;
    int	painchance;
    int	painsound;
    int	meleestate;
    int	missilestate;
    int	deathstate;
    int	xdeathstate;
    int	deathsound;
    int	speed;
    int	radius;
    int	height;
    int	mass;
    int	damage;
    int	activesound;
    int	flags;
    int	raisestate;
} mobjinfo_t;

extern mobjinfo_t mobjinfo[NUMMOBJTYPES];

Let’s provide an example with one of the Doom monsters, the Zombieman.

Here’s how the Zombieman’s mobjinfo is defined in the info.c file. Note that the Zombieman’s mobj type is MT_POSSESSED:

mobjinfo_t mobjinfo[NUMMOBJTYPES] = {
   .
   .
   .
   {
	// MT_POSSESSED
	3004,		// doomednum
	S_POSS_STND,	// spawnstate
	20,		// spawnhealth
	S_POSS_RUN1,	// seestate
	sfx_posit1,	// seesound
	8,		// reactiontime
	sfx_pistol,	// attacksound
	S_POSS_PAIN,	// painstate
	200,		// painchance
	sfx_popain,	// painsound
	0,		// meleestate
	S_POSS_ATK1,	// missilestate
	S_POSS_DIE1,	// deathstate
	S_POSS_XDIE1,	// xdeathstate
	sfx_podth1,	// deathsound
	8,		// speed
	20*FRACUNIT,	// radius
	56*FRACUNIT,	// height
	100,		// mass
	0,		// damage
	sfx_posact,	// activesound
	MF_SOLID|MF_SHOOTABLE|MF_COUNTKILL, // flags
	S_POSS_RAISE1	// raisestate
   },
   .
   .
   .
};

The attributes in the mobjinfo object define the states to transition to and the sound effects to play for a particular mobj type when entering the following core mobj states handled by the engine:

  • Spawn State – state to transition to when the mobj is spawned in the game. This is the “idle” state where the monster just stands and does nothing, checking its field of view until it spots a player to chase and attack.
  • See State – state to transition to when the mobj spots an enemy (in this case the player) and should chase it. Inside the code this is sometimes referred to as the “Chase” state.
  • Pain State – state to transition to when the mobj takes damage
  • Melee State – state to transition to when the mobj is ready to perform a close range melee attack. Only applies to mobjs that perform melee attacks. For the Zombieman, notice that the meleestate attribute is “0”, which corresponds to the NULL state, since the Zombieman doesn’t have a melee attack.
  • Missile State – state to transition to when the mobj is ready to perform a ranged attack. Only applies to mobjs that perform ranged attacks such as a Zombieman firing his rifle or a Cacodemon belching a fireball.
  • Death State – state to transition to when the mobj is killed
  • XDeath State – state to transition to when the mobj is killed via explosion (gibbed)

Now let’s look at the complete list of Zombieman state definitions as defined in info.c:

state_t	states[NUMSTATES] = {   
    .
    .
    . 
    {SPR_POSS,0,10,{A_Look},S_POSS_STND2,0,0},	// S_POSS_STND
    {SPR_POSS,1,10,{A_Look},S_POSS_STND,0,0},	// S_POSS_STND2
    {SPR_POSS,0,4,{A_Chase},S_POSS_RUN2,0,0},	// S_POSS_RUN1
    {SPR_POSS,0,4,{A_Chase},S_POSS_RUN3,0,0},	// S_POSS_RUN2
    {SPR_POSS,1,4,{A_Chase},S_POSS_RUN4,0,0},	// S_POSS_RUN3
    {SPR_POSS,1,4,{A_Chase},S_POSS_RUN5,0,0},	// S_POSS_RUN4
    {SPR_POSS,2,4,{A_Chase},S_POSS_RUN6,0,0},	// S_POSS_RUN5
    {SPR_POSS,2,4,{A_Chase},S_POSS_RUN7,0,0},	// S_POSS_RUN6
    {SPR_POSS,3,4,{A_Chase},S_POSS_RUN8,0,0},	// S_POSS_RUN7
    {SPR_POSS,3,4,{A_Chase},S_POSS_RUN1,0,0},	// S_POSS_RUN8
    {SPR_POSS,4,10,{A_FaceTarget},S_POSS_ATK2,0,0},	// S_POSS_ATK1
    {SPR_POSS,5,8,{A_PosAttack},S_POSS_ATK3,0,0},	// S_POSS_ATK2
    {SPR_POSS,4,8,{NULL},S_POSS_RUN1,0,0},	// S_POSS_ATK3
    {SPR_POSS,6,3,{NULL},S_POSS_PAIN2,0,0},	// S_POSS_PAIN
    {SPR_POSS,6,3,{A_Pain},S_POSS_RUN1,0,0},	// S_POSS_PAIN2
    {SPR_POSS,7,5,{NULL},S_POSS_DIE2,0,0},	// S_POSS_DIE1
    {SPR_POSS,8,5,{A_Scream},S_POSS_DIE3,0,0},	// S_POSS_DIE2
    {SPR_POSS,9,5,{A_Fall},S_POSS_DIE4,0,0},	// S_POSS_DIE3
    {SPR_POSS,10,5,{NULL},S_POSS_DIE5,0,0},	// S_POSS_DIE4
    {SPR_POSS,11,-1,{NULL},S_NULL,0,0},	// S_POSS_DIE5
    {SPR_POSS,12,5,{NULL},S_POSS_XDIE2,0,0},	// S_POSS_XDIE1
    {SPR_POSS,13,5,{A_XScream},S_POSS_XDIE3,0,0},	// S_POSS_XDIE2
    {SPR_POSS,14,5,{A_Fall},S_POSS_XDIE4,0,0},	// S_POSS_XDIE3
    {SPR_POSS,15,5,{NULL},S_POSS_XDIE5,0,0},	// S_POSS_XDIE4
    {SPR_POSS,16,5,{NULL},S_POSS_XDIE6,0,0},	// S_POSS_XDIE5
    {SPR_POSS,17,5,{NULL},S_POSS_XDIE7,0,0},	// S_POSS_XDIE6
    {SPR_POSS,18,5,{NULL},S_POSS_XDIE8,0,0},	// S_POSS_XDIE7
    {SPR_POSS,19,5,{NULL},S_POSS_XDIE9,0,0},	// S_POSS_XDIE8
    {SPR_POSS,20,-1,{NULL},S_NULL,0,0},	// S_POSS_XDIE9
    {SPR_POSS,10,5,{NULL},S_POSS_RAISE2,0,0},	// S_POSS_RAISE1
    {SPR_POSS,9,5,{NULL},S_POSS_RAISE3,0,0},	// S_POSS_RAISE2
    {SPR_POSS,8,5,{NULL},S_POSS_RAISE4,0,0},	// S_POSS_RAISE3
    {SPR_POSS,7,5,{NULL},S_POSS_RUN1,0,0},	// S_POSS_RAISE4
    .
    .
    .
};

Before diving deeper into these states, let’s go back and cover the tics concept that I mentioned briefly.

Gametics and Thinkers

Doom doesn’t manage the concept of game updates in terms of milliseconds since the last game update. A gametic is the equivalent of a single game update – basically one iteration of the Doom game loop. During this iteration the engine performs one frame update and executes basic game loop activities such as capturing input, executing gameplay logic and rendering to the framebuffer.

Doom’s engine is clamped at 35 tics per second (35 iterations of the Doom game loop every second), so a single tic should take no longer than 1000/35 = 28.57 milliseconds. This is the behavior of Vanilla Doom, however some source ports have removed this technical restriction and allow uncapped framerates.

To understand the game loop, lets look at some code. The Doom main game loop makes a call to the G_Ticker() function in g_game.c. A call to P_Ticker() (defined in p_tick.c) is made as long as the game is running a level (gamestate=GS_LEVEL):

//
// G_Ticker
// Make ticcmd_ts for the players.
//
void G_Ticker (void) 
{    
    .
    .
    .
    // do main actions
    switch (gamestate) 
    { 
      case GS_LEVEL: 
	P_Ticker (); 
	ST_Ticker (); 
	AM_Ticker (); 
	HU_Ticker ();            
	break; 
	 
      case GS_INTERMISSION: 
	WI_Ticker (); 
	break; 
			 
      case GS_FINALE: 
	F_Ticker (); 
	break; 
 
      case GS_DEMOSCREEN: 
	D_PageTicker (); 
	break; 
    }
}

P_Ticker() handles game updates for the following:

  • All Players in the game (via a call to P_PlayerThink)
  • Mobjs (via call to P_RunThinkers)
  • Other things that we don’t care about right now
//
// P_Ticker
//

void P_Ticker (void)
{
  .
  .
  .
  for (i=0 ; i<MAXPLAYERS ; i++)
	  if (playeringame[i])
	      P_PlayerThink (&players[i]);
			
  P_RunThinkers ();
  P_UpdateSpecials ();
  P_RespawnSpecials ();
  .
  .
  .
}

Let’s digest all of this for a moment. You might think that to run game updates for all mobjs in the game you would just call an update() method passing the mobj as a parameter and do this for every single mobj in the game.

Doom handles game updates for mobjs a bit differently. Every mobj in the game has a Thinker object that manages the execution of game updates per frame for that specific mobj. The structure of a Thinker object looks like this (declared in d_think.h):

// Doubly linked list of actors.
typedef struct thinker_s
{
    struct thinker_s*	prev;
    struct thinker_s*	next;
    think_t		function;
    
} thinker_t;

As mobjs spawn in a level, their Thinkers are created and added to a doubly linked list (hence the prev and next pointers). This happens in the P_SpawnMobj() function (in p_mobj.c):

//
// P_SpawnMobj
//
mobj_t*
P_SpawnMobj
( fixed_t	x,
  fixed_t	y,
  fixed_t	z,
  mobjtype_t	type )
{
    .
    .
    .

    mobj->thinker.function.acp1 = (actionf_p1)P_MobjThinker;	
    P_AddThinker (&mobj->thinker);

    return mobj;
}

Likewise, as mobjs are removed from a level (for example a projectile mobj hits a monster and needs to be removed after the explosion), their Thinkers are removed from the doubly linked list (P_RemoveMobj function in p_mobj.c):

//
// P_RemoveMobj
//
mapthing_t	itemrespawnque[ITEMQUESIZE];
int		itemrespawntime[ITEMQUESIZE];
int		iquehead;
int		iquetail;

void P_RemoveMobj (mobj_t* mobj)
{
    .
    .
    .
    
    // free block
    P_RemoveThinker ((thinker_t*)mobj);
}

Every Thinker object has a reference to a function (attribute think_t function) that executes an mobj’s game updates. This function is P_MobjThinker() in p_mobj.c:

//
// P_MobjThinker
//
void P_MobjThinker (mobj_t* mobj)
{
   // We'll go through this code later
}

P_RunThinkers() simply iterates through each mobj’s Thinker and executes its Thinker function – P_MobjThinker():

//
// P_RunThinkers
//
void P_RunThinkers (void)
{
    thinker_t*	currentthinker;

    currentthinker = thinkercap.next;
    while (currentthinker != &thinkercap)
    {
	if ( currentthinker->function.acv == (actionf_v)(-1) )
	{
	    // time to remove it
	    currentthinker->next->prev = currentthinker->prev;
	    currentthinker->prev->next = currentthinker->next;
	    Z_Free (currentthinker);
	}
	else
	{
	    if (currentthinker->function.acp1)
		currentthinker->function.acp1 (currentthinker);
	}
	currentthinker = currentthinker->next;
    }
}

We can think of P_MobjThinker() as the function called for each game state update for each mobj. This function will take care of movement, physics simulation and cycle between AI states.

We can summarize the above behavior with the following pseudocode:

G_Ticker() // g_game.c
{
	switch(gamestate)
	{
		case GS_LEVEL:
			P_Ticker() // p_tick.c
			{
				P_RunThinkers() // p_tick.c
				{
					// Iterate through all thinkers in level (linked list)
					foreach(thinker in thinker linked list)
					{
						function = thinker.function;
						// Execute thinker function
						// Runs P_MobjThinker() function in p_mobj.c
						function.run();
					}
				}
			}
	}
}

In the next article we’ll continue our deep dive into Doom AI game logic and the technical details of P_MobjThinker().

While you wait for the next article we recommend reading:

ARThings Released – Augmented Reality Doom

We recently released a Doom experience for Android’s ARCore titled ARThings. It was originally titled ARDoom but after receiving hate mail from the Google Play admins informing me that I couldn’t use the brand name “Doom” as part of the app title, I changed the name.

Click below to check it out!

We’re currently on v1.1 which has the following features:

  • Click on a horizontal tracked plane to spawn either 4 shotgun shells, a Zombieman or a Heavy weapon dude
  • Shotgun shells, Zombiemen or Heavy weapon dudes spawn randomly on detected horizontal planes
  • Blast away Zombiemen and Heavy weapon dudes with the proverbial Doom shotgun

Some technical facts:

Special thanks to:

  • John Carmack for answering some questions via email related to framerate clamping in the Doom Linux codebase
  • Fabien Sanglard for letting me proofread his upcoming Doom Game Engine Black Book. This was instrumental in helping me understand Doom monster AI