Open Game Source: Hexen(SDL)

by Dennis Payne

Name: Hexen (SDL)
Version: 0.2.3
Authors: Raven Software
License: Free for Non-Commercial
Operating System: Any SDL Platform
Website: http://www.devolution.com/~slouken/SDL/projects/hexen/index.html

Game

In the mid 1990s, Raven Software licensed the DOOM source and created two fantasy games. Hexen was the later of the the two and continues the story from Heretic:

While you were battling the evil forces of D'Sparil, the other Serpent Riders were busy sowing the seeds of destruction in other dimensions. Now, three brave souls have sworn to crush the evil regime that threatens to destroy the world forever. Find Korax's stronghold, destroy him and restore order in the physical world.

Commercial Source

Hexen, like most commercial programs, was never intended to be released in source form. Prior to Id Software's release of DOOM, releasing the source to old games was seen as madness. After Id, a handful of companies released some code. The pressures of real world concerns gives the source a different feel from open source projects.

On initial release, Raven Software made a self-extracting archive on the internet. They quickly realized that not everyone could run the self-extractor and released a zip file. Still they left the end user license agreement as a Microsoft Word file which is rather annoying. Although Raven has been fairly supportive of modifications and ports (1), the only email address in the source appears to be for legal questions on the license. This is not what you typically find in an open source project.

Since the official source is missing the sound library, this article focuses on Karl Robillard's port to SDL. I did look over some of the original source to determine how far the SDL version has progressed. For the most part it seems to have kept the original code base. Even so, the Hexen code was not built from scratch. They licensed the DOOM source from Id to build Heretic and Hexen.

When looking at source code, I usually start with header files since the structures and function prototypes can give a good overview. In this case I discovered multiple prototypes for some functions. For example I_RegisterSong has a prototype in i_header.h and i_sound.h. After a quick search I discovered six unused header files(2). In the previously mentioned case, i_header.h is the unused file even though it contains a short description for each function. The code is fairly clean and well structured otherwise(3).

Stating the Obvious

In h2def.h, the thinker structure is defined. The thinker is a linked list of function pointers. The functions decide the actions for all the game objects, from the bobbing health vial to the shield-carrying centaur. From this observation, the logical assumption would be that each monster has it's own thinker function. That's not the case.

All monsters share a single thinker function P_MobjThinker. Objects such as doors and raising floors have separate thinker functions. So how do the monsters have different reactions? Hexen uses a technique known as a state machine.

A state machine is a fairly simple concept and and easy to implement; it can also make a reasonable opponent. The machine is given some initial state. Every turn the machine looks at the state and determines what the next state should be. A simple monster could have four states: look for player, first move towards player, second move towards player, and attack. Your first thought would probably be to run everything in order and loop infinitely. Alternatively the creature could look for the player once, followed by an infinite repeat of move and attack until the player dies. The first implementation results in a simple grunt. The second will hunt the player to the ends of the earth if needed, ignoring all other opponents. A good state machine is obviously more complex and includes some randomness to prevent total predictability.

In Hexen, the states are defined in info.c. Each state includes the number of clock ticks spent in the state, sprite and frame information, a function to run, and finally what state to go to next. The action function is what actually does the appropriate task and may override the state information. For example A_MinotaurChase causes the minotaur to race after the target. If the minotaur is within range, it will skip the next state and attack immediately. There are over two thousand states in the game.

Eliza Bull

In general I'm not a big fan of first-person shooter. By adding conversation with the creatures in the game you could greatly expand the possibilities. The current conversation technique has a single key per player and communicates across all distances. Ideally, conversation should involve only those in the nearby vicinity. As an initial test I ignored the talking problem and added 'm' to speak directly to a minotaur summoned by the player.

The minotaur could have been easily turned into a robot that does exactly what it is told. Or a simple script could have been written. Instead, I decided on something a little more interesting. For my college artificial intelligence course we had to implement a simple version of the classic Eliza program in Lisp. A quick conversion of the function to rephrase patient's statements into a question, and the minotaur came to life. Well, he did up until the crash anyway.

The player's message buffer is eighty characters long. As part of turning the statement into a question a beginning sequence of up to twenty-eight characters may be added. This limited the player to fifty characters which still seemed reasonable. As it turns out though the screen display can only draw thirty-eight characters, after which the program crashes. For the time being I just chopped the response. A better implementation would be to store the words that won't fit and send them later.

Unfortunately sending the messages later wouldn't help very much because the player is limited to a single message on the screen. To make conversation truly useful in expanding the game, the player would need to view more than a single line of text at once. Additionally, after moving the message variables from player_s to mobj_s, I discovered a player does not have a defined monster object at creation. This may prevent the player from receiving messages in some circumstances. Low memory computers may have problems with the increased size of the monster object.

For anyone considering further enhancements I noticed that monsters contain linked lists to other creatures in the same sector and the same block. Depending of the size of sectors and blocks they could perhaps be used to send messages to everyone in an area. It would lead to break points where you can't speak to someone just a few pixels past the sector break, but it wouldn't require much additional coding. Here is the current patch.

End Notes

Even though there were several flaws that prevent my Eliza patch from being a complete success, prototyping conversation proved to be a fairly simple addition. There was some additional hackery I found such as storing a pointer to the summoning player's mobj_t in an integer. Still the programmers at Raven managed to exceed my expectations on Hexen. Which is perhaps a little unfortunate for those who come afterwards.

Footnotes

(1) There are several other ports available. I tried to chose one that remains close to original source.

(2) Unused header files: ct_chat.h, drcoord.h, dstrings.h, i_cdmus.h, i_header.h, and vgaview.h.

(3) In my initial overview, I stumbled across two functions with nearly the same functionality, M_ForceUppercase in m_misc.c and strupr in w_wad.c. They both convert strings to uppercase in a slightly different manner but I didn't get a chance to explore why.

Back to OGS


Copyright (c) 2000 Dennis Payne / Identical Software