Getting Started with GZDoom Modding
If you are reading this on the github repo, try this link for some HTML
styling. Then use the floating element labeled “Table of Contents” to the
top-right of this page to navigate between sections by clicking on them.
I’ve removed the scrollbar, so that is the only way. Click on the “Switch
Color Mode” text at the top-left of the page to switch between light and
dark modes. Zoom-in via browser for bigger font size.
This guide will introduce the basics of modding for the GZDoom source port. It is aimed at beginners with little-to-no technical knowledge. It will seem tedious to people with some proficiency with coding or GUI experience. For more succinct or advanced help, check the Tutorials page and the How-to-Start-Modding page on the zdoom wiki, or Jekyll Grim’s ZScript basics. The guide will focus on GZDoom, but might have limited applicability to other source ports. It is structured as a sequence of exercises that the reader is expected to follow along by actually implementing the instructions on their computer. It is meant to be done at a relaxed pace over several sessions spanning days, and not in a single sitting. It presents particular ways of doing things in particular chapters for pedagogical reasons and to inculcate good habits, like how one only practices scales for the first few weeks when they start learning a new musical instrument (see also: WAX ON, WAX OFF). It will not present the full context or historical information for everything, because it is meant to be a first-pass curriculum (see table of contents, top-right), where too much extraneous information can be overwhelming and counter-productive. The reader won’t be provided with too many choices for ways to do something at first to avoid decision paralysis. A stable foundation will first be built so that the reader will have somewhere to retreat to when they are lost.
You can recommend additions/changes to this guide by submitting PRs to
the github repo. Please only make changes to the index.org
file. Do
not edit the html
file directly. And do not expand the scope too
much.
Running GZDoom and understanding WADs
Getting the Engine
First, let us get GZDoom running and playing a game. Download the latest version of GZDoom (and not ZDoom) for your operating system from here: https://zdoom.org/downloads
If you are running on Linux, you might be better off compiling it from source. That is not a ding. I do this myself.
After downloading and unpacking, you should end up with a folder somewhere on your computer with files named:
gzdoom.exe
(or justgzdoom
)gzdoom.pk3
lights.pk3
brightmaps.pk3
game_support.pk3
game_widescreen_gfx.pk3
fm_banks
(folder)soundfonts
(folder)
Depending on the OS you downloaded for, you might have some extra files
like *.dll
files. If you try to run the executable file (gzdoom.exe
) by
either double-clicking on it, or by running the command from a
terminal/command-prompt from that folder location, it should throw up a
message saying that it couldn’t find any IWADs.
To run as a command, open a terminal (Linux) or command prompt (Windows),
and navigate to the directory location using the change-directory (cd
)
command. When there, list the files using the ls
(Linux) or dir
(Windows) command. It should print out all the files from the above list.
Then from there, just type ./gzdoom
(Linux) or gzdoom.exe
(Windows).
This has the same effect as double-clicking the file from your graphical
file browser. Keep the terminal/command-prompt open (minimize it for now).
We won’t be using the command method everytime but it helps with
understanding what is happening under the hood.
Getting some IWADs
Running the engine executable by itself couldn’t run a game because you haven’t downloaded any actual games for the engine to run. Let us fix that. For this guide, we will be using Freedoom, but you could follow the same steps with Doom 1 and Doom 2 if you have purchased those wad files from you favorite legal videogame store.
Acquire the files freedoom1.wad
and freedoom2.wad
from
https://freedoom.github.io/download.html (click on the Freedoom:
Phase 1+2
hyperlink). Or better yet, get version 0.13.0 specifically
from here, since that is the one this guide will be using. Extract
these two *.wad
files from the zip archive and place them in the
same directory as your GZDoom executable. If you are having trouble
opening a zip archive file, double-click or right-click on it and
“extract” the contents. On the Linux terminal, a tool like unzip
can
also be used. With the files placed at the same location as your
GZDoom executable, running GZDoom again should cause the program to
auto-detect freedoom1.wad
and freedoom2.wad
and list them for
selection on startup.
Let’s run Freedoom 2 from the command line. From the terminal/command-prompt, run the following command:
- Windows
gzdoom.exe -iwad freedoom2.wad
- Linux
./gzdoom -iwad freedoom2.wad
The game should launch, and you should be greeted with these starting screens of the main menu, and the first level:


These screenshots are from running version 0.13.0 of freedoom2.wad
.
Another version might look slightly different. And while we are here,
go back into the main menu, go to Options
, then enable Full Options
Menu
. In here, go to Display Options -> Texture Options -> Texture
Filter mode
and set it to “None
”. Thank me later.
The files freedoom1.wad
and freedoom2.wad
are called IWADs, which is
short for internal WADs. These are wad-files that have all the information
within them for the engine to run a whole game. Later on, we will take a
look into what is actually inside them by using another program.
Basic replacement mods
Texture replacement
Now we are going to make our first mod. Create a project folder
somewhere on your computer. Call it something like
“my_first_mod
”. Inside of it, create a folder named
“textures
”. The name of this folder is important. It has to be spelt
exactly and cannot be arbitrary. Now, right click the following image
file and save it inside the “textures
” folder. Make sure that it is
named AQRUST08.png
. The filename is important.

AQRUST08.png
modified wall texture
Now, zip the textures
folder into a zip-archive file called
something like my_first_mod.zip
. The filename of this zip archive is
not important and can be anything of your choosing. You can
right-click the folder and compress it into a zip file. On Linux, you
can run this command from the location of your project folder:
zip -r my_first_mod.zip textures/
This uses the zip
program with the -r
recursive flag to make sure that
contents of subfolders end up inside the zip file. Congratulations. As far
is the current version of GZDoom (4.14.0 as of this writing) is concerned,
you just made your first mod. Now let’s run it. You can do this either by
dragging and dropping the zip file onto your gzdoom.exe
executable file,
and selecting the freedoom2.wad
IWAD when it asks, or by running the
command:
./gzdoom -iwad freedoom2.wad -file <PATH-TO-PROJECT-FOLDER>/my_first_mod.zip
Once you launch the game, you should be greeted to this view:

Analysis
Let us unpack what is happening here. You ran GZDoom with two files:
freedoom2.wad
and my_first_mod.zip
. The first one is the IWAD,
meaning the file that contains the base game. The second one is what
is called a PWAD (short for patch WAD). The IWAD contained a texture
lump (will explain later) named AQRUST08
, and a map lump (among
others) named MAP01
, and instructions inside the map lump for the
engine to paint that texture on a specific bunch of walls (with some
offsets, lighting effects, etc.). This is what happens when you run
GZDoom with just the IWAD freedoom2.wad
by itself. But when you run
the IWAD with this PWAD, which contains its own copy of the AQRUST08
texture (we’ll get into how I knew that later), the engine replaces
all instances of that texture with the one from your PWAD. This is
also why it was important to name the file exactly right. If you load
two PWADs that both replace the same texture like so:
./gzdoom -iwad freedoom2.wad -file mod1.pk3 -file mod2.pk3
then the replacement texture from the last PWAD (mod2.pk3
) will be used.
Think of it as if the replacement instructions are executed serially, in
the sequence of the files specified. For the rest of this guide, we will be
naming the zip-archive file my_first_mod.pk3
, but always remember that
under the hood it is merely a zip file.
Get setup with a Launcher before proceeding further
This is one reason I illustrated the command line method of launching GZDoom. Selecting multiple mod files and dragging-and-dropping them onto the GZDoom executable does not give you control over the mod load order. People routinely play games on GZDoom with 5-15 mods loaded at once, often in a specific order. Don’t worry. No one is actually typing out long commands. They are using launcher programs like ZDL or DoomRunner. I myself am partial to DoomRunner. Pick one and stick to it.
Most launcher programs allow saving of presets for various mod and order combinations. On first launch, they typically ask you to select engine executables, a list of IWADs, and the usual location for various kinds of PWADs like map packs. So you don’t have to put files in your GZDoom folder, or any other specific location. You should feel free to organize your files on your computer any way you see fit. Here is a view of my DoomRunner front page:

As you can see, I have highlighted a preset that I have named
“Elementalism
” that is using the GZDoom executable, the doom2.wad
IWAD file, and a whole bunch of PWADs in a particular order. The
lights.pk3
file from the GZDoom install is for dynamic lights (it
doesn’t get loaded by default for performance reasons). Elementalism
is an ambitious map pack, and Hellrider Vengeful
is a weapons and
player-movement mod. Here, I am adding a mod called Flashlight++
even though Hellrider
already comes with a flashlight, because the
maps in Elementalism
have all been programmed to strip the player of
all inventory items and pistol-start every level. And the flashlight
in Flashlight++
happens to be unclearable using that method. If I
were to try and and another mod that modifies weapons, like
Beautiful
Doom
to this list, then the conflict with Hellrider
will cause
all weapons to be replaced by one mod and ammunition pickups be of the
other. So not all mods are designed to go together. I mostly ignore
DoomRunner’s separate map pack subwindow and load map-pack mods as
regular mods, with full control over load order.
For the rest of this guide, I recommend picking a launcher program and
launching GZDoom with my_first_mod.pk3
and freedoom2.wad
just to
develop good habits. Under the hood, all these Lauchers are just
constructing and executing lengthy commands like the ones above.

Remember that my_first_mod.pk3
is really just a zip file. The file
extension doesn’t matter, and only exists to help you. Modern GZDoom PWADs
are named *.pk3
and IWADs are named *.ipk3
(we’ll get there). You might
recall that the engine’s internal files that came with the GZDoom download
(lights.pk3
, brightmaps.pk3
, game_support.pk3
, etc.) are also *.pk3
files. These are the only ones that should not be moved out of the GZDoom
executable’s folder.
Older mods, and mods made to be interoperable with source ports other
than GZDoom aren’t zip files, but are instead of the WAD format. While
GZDoom can read WAD files (the IWAD freedoom2.wad is a WAD file, after
all), the best practice is to make mods as *.pk3
files (which are
secretly zip files). Only levels/maps need to be in the old WAD
format. More on that later.
Text files as lumps
Let us continue adding to your mod. So far, you have a zip file (now named
“my_first_mod.pk3
”) which contains a folder named textures
, which
in-turn contains a png
image file named AQRUST08.png
. It was important
for this file to be a png
file. And it is very important that both the
folder and file names are what they are for the mod to work. The filename
had to be AQRUST08
because that tells the engine what texture it is meant
to replace. The folder name textures
is a reserved name. The engine (and
most map editors) interpretes it as a location for texture files. You can
put any png
images in them, and even organize them into subfolders
within. But you cannot put other arbitrary data in it and expect it to
work. Nor can you just put the AQRUST08.png
file alone in a zip archive
and expect the replacement to work.
There are other reserved names, as far as files and folders in the
root (top) location of the zip archive is concerned. These reserved
names can have any capitalization. They can be camelcase, all upper
case, or lower case. It is all the same to the engine. Reserved names
for folders include “textures
”, “flats
”, “sprites
”, “maps
”,
etc. A full list is on the wiki. You can have other folders and
subfolders, but these and their contents are treated by the engine in
a special way. Reserved names for files include TEXTURES
, ZSCRIPT
,
MAPINFO
, GLDEFS
, etc. Again, the capitalization doesn’t matter,
and you can give them any file extensions you want (*.lmp
, *.txt
,
*.zsc
). You can store other files in the root location of your mod
(like a license file, a readme, or a credits file if you end up using
other people’s work), but they cannot use these reserved names.
Files in *.pk3
archives may be referred to as lumps as a holdover
from the wad-format days. The wad-format is also a sort of archive
format like zip
. Contiguous sequence of bits inside a wad-file
(often marked with a start
and end
markers) represent specific
types of data. We can explore this later, but we don’t have to worry
about that as long as we have subfolders and reserved names. These
special lumps are often just text files that have their own format for
presenting data to the engine. Let us try and use one to get a better
feel.
For this next exercise, we will replace the texture on the door visible
from the starting area in Freedoom 2. The lump name for this texture needs
to be BIGDOOR1
(again, I’ll get to how I know this later). But we will
not just be using a file named BIGDOOR1.png
in the textures
folder,
even though that will work. We will instead use some other file name, and
try to use the TEXTURES
lump to make it work.
Pick any png
image file you want. I’m going with John Romero’s forehead.
Crop/scale the image using some image manipulation program like GIMP,
Photoshop, or even MSPaint, to fit into 128 \(\times\) 96 pixels to match the
door’s texture size. I named my file John_Romero.png
, and I placed it
inside a subfolder inside the textures
folder called custom
. Then, open
a text file called TEXTURES.lmp
in the project root location. Put the
following lines into it (replace the path and file name in the Patch
line
to match your new image):
Texture BIGDOOR1, 128, 96
{
Patch "textures/custom/John_Romero.png", 0, 0
}
The commas and the curly-brackets {
& }
are important and not just
for show. Now re-compress the zip archive with these two new files and
the new subfolder.
zip -r my_first_mod.pk3 textures/ TEXTURES.lmp
The archive’s internal structure should look something like this:

Now, running the PWAD with the freedoom2.wad
IWAD (through a
launcher like DoomRunner) should bring up this view:

What the TEXTURES.lmp
file (or the TEXTURES
lump) did was create a
new, virtual texture container with the name BIGDOOR1
with the image
John_Romer.png
patched in, and presented that to the engine. You can
create new virtual textures this way by combining and mashing multiple
other textures together (yes, even other virtual ones). You can scale,
rotate, skew, mirror/flip, and mask, as well as do other kinds of
transformations without actually creating new image files to be stored
in the PWAD. The TEXTURES
lump is a great way to put a decorative
poster or graffiti onto an existing wall texture. An example we will
go through later will involve slapping an interactable switch onto a
wall texture for use in a custom map.
These lump names, you will notice, have both been eight characters long
(AQRUST08
, BIGDOOR1
). This is another hold-over from the DOS days,
where file names would be truncated to eight characters. The case doesn’t
matter, but you have to stick to this convention while naming lumps. Later
on when we get into map making, you will find that certain map formats
allow for use of full texture filenames with full paths instead of these
short lump names. It is still recommended that you use these lump names
instead. Because this makes your maps easily moddable (by you as well as
others!) if the lump names are standardized.
Basics of SLADE
Next, we will introduce another helpful program to our modding toolbelt. This one is a WAD-editor called SLADE. You can acquire it for your preferred OS from here or here. SLADE is in principle capable of many things. You can write code, compile code, paint textures, create and modify brightmaps, and even make/edit levels. There are Doom mod authors who develop entirely on SLADE (some of them livestream the process). But in this guide, as a rule, we will only be using SLADE to take a peek into WADs, and possibly extract content. We will not be using SLADE to modify any data within wads. Feel free to learn its intricacies on your own, later.
To not overwhelm ourselves, let us first use SLADE to open the simplest mod
we have: “my_first_mod.pk3
”. Here is the view you should be presented
with:

It’s all fairly intuitive. There is a panel that shows the files or
“lumps” in your “WAD” (I’ve expanded the folders and subfolders), and
a bigger panel to the right that shows the content of the currently
selected lump. In the image, I have selected the TEXTURES.lmp
file/lump to show its textual content. A few things of note here is
the SLADE has correctly identified the file-types of our lumps as two
PNG
Graphic files and a “Texture Definition” lump. It says so next
to the file as well as in the bottom bar of the window. Furthermore,
it has auto-selected the “ZDoom Textures” option in the “Text
Language” pulldown menu above the textual-content panel for syntax
highlighting. All of these areas of the SLADE window are important to
us. You can select the AQRUST08.png
file and watch the image
displayed in the content panel, along with the image size in the
bottom bar.
Now let’s open a bigger “WAD” file: freedoom2.wad
. Be sure to create
a backup of this file before opening it in SLADE, just to avoid
accidentally causing a change in it. That would violate our rule
regarding SLADE for this guide.
Since this is a wad-format file, you should be confronted with a
lengthy, flat list of lumps with no hierarchical subfolder
structure. The lumps are by default, ordered in the way they are
stored in the wad (the ordering is important in the wad format). But
you can click on the “Name” tab at the top of the lumps panel to
display the lumps in ascending order of their name strings. If you
scroll down to Freedoom 2’s version of the AQRUST08
graphic (the one
your mod replaced), you will see that the “Type” field says “Graphic
(Doom)” instead of “Graphic (PNG)”. The image is stored in a Paletted
raw format. If the image appears in black-and-white in the contents
panel, you can instruct SLADE to use Doom’s color palette in the
drop-down menu to the top-right. The same goes for exporting
graphics. You can’t just right-click on the AQRUST08
lump and click
export, as it will result in a binary lump file. You would have to
navigate to the sub-popup menu under “Graphic” after you right-click,
and select the “Export as PNG” option. If you’d like to practice
further, try exporting the SLIME14
graphic as a PNG
file and
modify it, then include it in my_first_mod.pk3
. This should apply to
the floor in the opening area of Freedoom 2.
Another curiosity that should be of interest is that there is no lump
named BIGDOOR1
(at least as of Freedoom 2 version 0.13.0). There is
one called DOOR2_1
that looks suspiciously like the door from the
opening level, but it is too small (96 \(\times\) 96 pixels, see the bottom
bar). You can test that it isn’t the right one by attempting a
DOOR2_1
lump replacement file in your mod. If will replace all
instances of DOOR2_1
usage in the levels of Freedoom 2, but all
BIGDOOR1
instances (including the one in the opening area) will
remain unaffected.
Freedoom 2 is actually defining the BIGDOOR1
lump inside its
TEXTURE1
lump. You can scroll to it and highlight it with a
click. This is stored in the older wad-centric format here and not as
a text file (unlike in my_first_mod.pk3
). Which is why the “Type”
field says “TEXTUREx”. But SLADE lets you edit it if you click on the
“Edit Textures” button that should have appeared in the content
panel. Clicking on it should open this lump in its own tab and present
you with a list of virtual textures defined within.

Scrolling this list and highlighting BIGDOOR1
should reveal to you
(in a “Patches” panel to the right) the five patches used to make this
lump. There’s four copies of the W13_1
patch with the corresponding
offsets forming a background canvas of size 128 \(\times\) 96 pixels, and one
instance of DOOR2_1
patch slapped on top at the center. You can look
at the W13_1
graphic lump by tabbing back to the freedoom2.wad
tab. If you’ve been following this guide in detail, you get exactly
what is going on here.
Sprite replacement and offsets
So far, we have made basic replacements for textures. Next, let us
replace some sprites. You would think that this is more of the same,
but there is a subtletly that lets us learn a couple of new
concepts. Get back into the game Freedoom 2, and turn left in the
starting area. You should see two health-pickup items called
“StimPacks
”. I have circled them in green in the following
screenshot image:

These are a pickup item that can restore up to 10 points of
health. They can be picked up by walking over them. But your health
(indicated in the HUD below) needs to be less than 100 points for a
successful pickup. You can lose some health points by going forward in
the corridor and deliberately getting shot at by the two enemy zombies
in the cubby-room to the left of the door. Returning to the StimPacks
and walking over them should restore up to 10 points. Just walking
around the StimPacks
, you will notice that they are depicted on
screen by a single sprite/image that always faces your viewpoint
(meaning you can never see “behind” the sprite by walking around it).
The sprite used by this object (the technical term is actor
) is a
lump called STIMA0
. You can find it inside freedoom2.wad
via
SLADE. You would have to right-click and use the “Graphics” sub-menu
popup and “Export as PNG” if you want it as an image. To replace the
STIMA0
sprite with one of your own in my_first_mid.pk3
, you will
have to create one of the reserved-name folders called sprites
and
place your image with the name STIMA0.png
inside it, and then make
sure to zip it with the others. If you are having trouble
finding/deciding on a replacement sprite, you can try this red-colored
version. Just right-click and save the png image inside the sprites
folder of your mod.

zip -r my_first_mod.pk3 textures/ TEXTURES.lmp sprites/
If you are using some other image, you can try and match the original
lump’s size, which was 19 \(\times\) 10 pixels (check this in the bottom
bar inside the contents panel in SLADE). But you don’t have to match
it. So now the file structure inside of my_first_mod.pk3
should look
like this:

If you start the game now, and turn left at the starting area, you
will be surprised to find … nothing! But the two StimPack
actors
are actually there. You can confirm this by losing some health (get
shot at) and running back here to “pick” them up. However, you can’t
see the new sprites. In truth, they are below the floor. By default,
sprites in GZDoom are offset relative to the top-left corner of the
image file. You can add the correct offsets to PNG
files using SLADE
(they get stored in the PNG
format’s grAb
chunk) but we won’t do
that. For one, it violates our rule about not using SLADE to modify
things. And secondly, it would have to be redone everytime you replace
or modify the image file using some other third-party program. It
might not seem like much for a single sprite, but once you have
hundreds …
Let’s instead use our already existing TEXTURES.lmp
file. Add the
following lines to it (i.e. append below the existing lines), and
then re-zip the archive:
Sprite STIMA0, 19, 10
{
Patch "sprites/STIMA0.png", 0, 0
Offset 10, 10
}
Note the new Offset
field here. Change the numbers (as well as the
overall size numbers) according to your choice of image. There is also
a Scale
field that you could use if your image is much larger than
19 \(\times\) 10 pixels. The TEXTURES
lump uses inverse scale (2 = 50%
of the original size, 0.5 = 200% of the original size, etc.). With the
offset specified in the TEXTURES.lmp
file, now you can modify the
PNG
file as much as you want without worrying about preserving or
resetting the offsets within that file. The sprite replacement should
work in the game now.
ZScript class replacements
Now we will learn the very basics of a scripting language called ZScript
.
GZDoom understands multiple scripting languages for modding. But most of
them are deprecated and are only supported for backwards compatibility with
older mods. For modern GZDoom, there are only two scripting languages you
need to learn: ACS
and ZScript
. This guide won’t be going into ACS
,
but know that that is mostly only used for map specials (conditional
triggers and scripted sequences). For everything else, stick to ZScript
.
There are more advanced, and comprehensive guides to learning ZScript
:
Ash’s ZScript basics and David Newton’s Youtube Tutorials. In this guide,
we will show the use of ZScript
to modify two things present in this
animated view from Freedoom 2, visible after you turn left at the big door
in the starting area:

HealthBonus
items, and the Zombieman
enemyNew pickup item
First, let’s replace those green, flashing bottles from the image above.
That is a HealthBonus
. Unlike the StimPacks
from before, these give the
player 1 point of health all the way up to a maximum of 200 points. Meaning
that you can pick them up even if you have a 100 health points. Try doing
this. There are two more HealthBonus
items to the left of the view which
can be picked up without alerting the two Zombieman
enemies, as long as
you don’t fire your weapon or bump into them (or get into their light of
sight).
Create a text file named ZSCRIPT.zsc
in your project folder (the
extension doesn’t matter. ZSCRIPT
is one of those reserved lump
names). In it, put the following text:
version "4.14"
class MFM_Elixir : HealthBonus replaces HealthBonus
{
Default
{
Inventory.Amount 5;
RenderStyle "Shaded";
StencilColor "Red";
}
States
{
Spawn:
BON1 ABCDCB 3;
Loop;
}
}
The syntax here is different from what we’ve seen so far in
non-zscript files like the TEXTURES
lump. The semicolons “;
” at
the ends of some of the lines are very important. If you know some C
or C
-like programming language, this should be familiar. The colon
(“:
”) after the word “Spawn
” is deliberate and not a typo. Now
re-zip the archive with the new file:
zip -r my_first_mod.pk3 textures/ TEXTURES.lmp sprites/ ZSCRIPT.zsc
and launch the game with your mod (I hope you have habituated to using
a launcher by now). Go close to the big door and turn left to the same
location as before. All of the green HealthBonus
items should now be
replaced by translucent, red bottles that are flashing at twice the
rate. And if you pick one of them up, they should give you 5 health
points instead of 1.
Analysis
Let us go through the contents of the ZSCRIPT.zsc
file to see how
this is achieved. The first line reads version "4.14"
. This is a
necessary clause at the beginning of the ZSCRIPT
lump, and signals a
minimum version that your mod can now be run in. If you try and load
my_first_mod.pk3
in GZDoom 4.13.2 with the ZSCRIPT.zsc
file in, it
should throw an error. This facility exists to ensure that mods that
use new, advanced ZScript
features don’t get accidentally launched
by older versions of the engine, which would result in a crash.
Looking further down, even if you have no experience with programming,
you can notice the pairs of curly brackets encapsulating content,
which has been formatted with TAB
-indentations for clarity. There is
a master-pair of curly brackets “open” after the line that starts with
the term “class
”, and encapsulate everything else. This “everything
else” is in the “class
” “block”, or belongs to the “class
”. Within
the class
, there are two other blocks: the Default
block, and the
States
block. All of these are reserved keywords that the engine
assigns special meaning to.
Classes are a very common structure within programming languages, and
their complete definition is beyond the scope of this guide. But in
this limited context, a class
is a … well … class of entity that
can exist in the game’s simulation. A class
can be of various types:
inventory item, monster, the player’s character (called a
PlayerPawn
), a flying rocket, a falling rain drop, or even an
invisible “thinker”. Every entity within the game is an instantiation
of some class
. The engine “ticks” about 35 times every second, and
during each “tick” it runs through the list of entities on the map and
runs some standard “Tick()
” functions belonging to their class
definitions. There is a little more to that, but this is basically how
the game runs. Physics collisions, actors interacting with each other
and the map, etc., all happen in “ticks”, and there’s 35 of them in
about a second.
If the last paragraph was hard to understand, that is okay. The most
important thing about classes is that you can inherit properties from
a parent class
and then modify them. For the purposes of modding,
the syntax for declaring a class
object is:
class <NEW-CLASS-NAME> : <PARENT-CLASS-NAME> [ replaces <SOME-OTHER-CLASS-NAME> ]
The part between the square brackets [
& ]
is optional (the square
brackets themselves shouldn’t be typed). A new class
doesn’t have to
replace an existing class
. The new definition can exist and operate
independently. To use our own example, the line reads:
class MFM_Elixir : HealthBonus replaces HealthBonus
Here we have defined a new class
with the name MFM_Elixir
. The
prefix MFM_
stands for “My first Mod” (the name of your mod). It is
good practice to add a unique prefix to all new class
definitions. That way, there will be no conflict when your mod is
loaded along with another mod that might happen to have its own elixir
class
, since that is a common word. Anyway, this new class
is of
the type HealthBonus
which plays the role of the parent class
that
all properties can be inherited from. This HealthBonus
class
is an
existing class
that is defined inside the engine itself. You can
take a look at its definition in GZDoom’s source code here. The class
HealthBonus
has the class Health
as its parent class (further down
in the same file, you can see the definition for the StimPack
class,
which also has Health
as a parent class). You can find the same
class definition inside the gzdoom.pk3
file that came with the
engine download. Find it using SLADE
(zscript->actors->doom->doomhealth.zs
), but remember the rule: do
not modify the file. GZDoom automatically loads gzdoom.pk3
when you
run it (which is why it should always be present the same folder as
the executable).
Looking at the first few lines of class HealthBonus
, you can see its
Default
and States
blocks. We inherit these properties, and change
some values within the Default
block of our MFM_Elixir
class
definition. Namely, we changed something called Inventory.Amount
from 1
to 5
(to make it give you 5 health points on pickup), and
changed some other internal rendering properties like RenderStyle
and StencilColor
which affects how its sprites are rendered. And
lastly, the States
block is meant to contain the actor’s state
labels, which contain a state sequence. And each state in the sequence
is specified by the sprite to display, the time (in “ticks”) to remain
in this state for, and any other functions that need to be run. In our
example, MFM_Elixir
class’s States
block has a single state label
(Spawn
) just like its parent HealthBonus
class. This marks the
state sequence that all actors first enter when they are spawned on
the map. The sequence line reads:
BON1 ABCDCB 3;
This tells the engine to display the sprites BON1A0
, BON1B0
,
BON1C0
, BON1D0
, BON1C0
, and BON1B0
again in that order, each
for 3
ticks. The Loop;
statement in the next line causes this
sequence to repeat indefinitely. You can find these sprites inside
freedoom2.wad
using SLADE. The original HealthBonus
class
displayed each of these sprites for 6
ticks, which is why it was
flashing at half the speed of MFM_Elixir
.
Lastly, the replace HealthBonus
clause instructed the engine to
replace all instances of the HealthBonus
item on any loaded map with
MFM_Elixir
. The replaced class doesn’t have to be the same as the
parent class. You could, if you wanted to, replace all instances of
the Zombieman
class with MFM_Elixir
. You can test the replacement
effect by using console commands. Open the console while playing
Freedoom 2 (by hitting the tilde “~” key below the Escape
-key on
your keyboard). This should pause the game and give you a command
prompt. In it, type “summon MFM_Elixir
” (without quotes) and hit
ENTER
. Now close the console by hitting the “~
” key
again. Provided that there is enough space in front of your player
character, an instance of MFM_Elixir
will have been spawned in front
of you and will have fallen to the ground. If you repeat the exercise
and try to summon HealthBonus
instead, the replacement clause will
cause another instance of MFM_Elixir
to fall at your feet.
Let us remove the replacement clause by either deleting the words
replace HealthBonus
or by commenting them out by prefixing a
double-slash like so:
class MFM_Elixir : HealthBonus // replaces HealthBonus
All text in a line that follows a double-slash “//
” is treated as
non-existent by the engine. So this is a great way to leave comments
or notes-to-self all over your ZScript code to help yourself (and
others) understand the code better. After doing this, you should be
able to summon HealthBonus
and summon MFM_Elixir
via the console
and watch both items flashing next to each other like so:

Let us leave the mod in a state where MFM_Elixir
is NOT replacing
HealthBonus
for now, as we will be using this to learn something new
in the “Basics of mapping” section later.
Homework assignment
The sprites that we have explored so far: STIMA0
, and the
BON1[ABCD]0
, all share a common feature. They all end in a “0”. The
sprite names start with a 4-character string, followed by a
single-character frame ID, and then a number. The two items that these
sprites depict: StimPack
and HealthBonus
, both look the same from
all directions. But we have seen sprited objects/actors in this game
that look different from different angles (like the Zombieman
). Let
us try and understand how this is done. Let us replace the
MFM_Elixir
sprite sequence with another set of sprites from
freedoom2.wad
that also have the [ABCD]
frames, but do not end
with a “0” character. Replace the state-sequence line with:
HEAD ABCDCB 3;
Get back into the game and summon MFM_Elixir
in a brightly lit
region. Now walk around it and see how the sprite changes based on the
angle. To make it clearer to see, comment out the RenderStyle
line
in the Default
block and use a single sprite frame to keep it from
animating:
HEAD A 1;
Now actually read the Angles
and Mirroring
sections from the
Sprites
wiki page. Look up the names of all the sprite lumps that
start with the characters HEADA
(there should be five of them). See
if the names make sense in terms of in-game viewing angle. Do not
proceed further until you have understood how sprite-naming convention
can affect sprite rotations. You can get pretty close to smooth
rotations if you use all 16-rotation characters, but at that point,
especially if you have a lot of animations, you are better of learning
to use 3D models.
Before proceeding, I want to confirm that you are not modifying the
internal contents of my_first_mod.pk3
archive file using SLADE, but are
directly modifying the files outside in your project folder and
re-compressing the zip archive. The importance of this habit will become
clear later.
Modifying Zombieman
Okay. Now we do enemies. Boot up the game. You see these two former
gentlemen admiring the flashing HealthBonus
bottles:

You can alert them by either bumping into one of them or firing your
weapon. Even punching the air with your fist (weapon-slot 1) should do it.
Get a feel for their behavior. Watch how they move around, and stop to
shoot. With clever positioning, you can even get one of them to
accidentally shoot the other and cause some in-fighting among them. After
you’ve gained some feel for their behavior, take a gander at their class
ZombieMan
definition in the engine source code (or inside the gzdoom.pk3
file). It has a master parent-class simply called Actor
, which has a
parent-class called Thinker
, which is doing most of the heavy lifting.
ZScript
is powerful enough to allow you to define any general sort
of game actor with very unique, custom behavior. But since GZDoom
started life as a Doom source port, it supports Doom-style functions
and behavior out of the box. ZombieMan
uses a very small set of
built-in functions: A_Look()
, A_Chase()
, A_FaceTarget()
,
A_PosAttack()
, A_Pain()
, A_Scream()
, A_NoBlocking()
, and
A_XScream()
. These functions rely on values of some default fields
to perform some actions and kick the actor into one of the standard
state labels. The standard state labels defined for the ZombieMan
are Spawn
, See
, Missile
, Pain
, Death
, XDeath
, and
Raise
. These (along with some others used in interactive actors) are
built in for the standard functions. You can define a custom damage
type named XYZ
, for example, that can have its own Pain.XYZ
and
Death.XYZ
states to transition to if they were defined (the default
Pain
and Death
states are used otherwise). XDeath
is entered
when damage incurred exceeds a certain threshold value. Raise
comes
in when a dead actor is resurrected using a built-in method.
The ZombieMan
has very simple, basic, AI. A_Look()
makes it look
for hostiles. A_Chase()
makes it turn 45-degrees in a random
direction (biased towards its target
actor) when ever it gets called
(or the actor collides with something). The rest should be
self-explanatory. You can read the wiki pages that I have hyperlinked
for more. Suffice to say that GZDoom default AI functions have no
concept of path finding or advanced goals and behaviors. Modders had
to code it all themselves. Before the days of ZScript
this was done
using token inventory items. Although, at the time of this writing,
GZDoom got some code updates for a behaviors subsystem. So in the
future, we should see that getting used (and someone will need to
tutorialize it).
You can define your own state labels, but will need custom functions
to handle state transitions into and out of them. Let us now create a
replacement class named MFM_InviZombie
to make this enemy slightly
more exciting. You can add these lines to the same ZSCRIPT.zsc
file
below the MFM_Elixir
class definition. Let is add/modify the state
labels one at a time. For starters, we will split the See
state
label into two:
class MFM_InviZombie : ZombieMan replaces ZombieMan
{
Default
{
StencilColor "DDDDFF";
Speed 12;
}
States
{
See:
POSS A 1 A_StartSound("brain/cube", starttime: 0.6);
POSS A 4 A_SetRenderstyle(alpha, STYLE_Shaded);
POSS A 4 A_SetRenderstyle(alpha, STYLE_Fuzzy);
See2:
POSS AABBCCDD 4 A_Chase;
Loop;
}
}
So here, I gave the class a default StencilColor
(a blue-ish white) and a
Speed
of 12
(the default ZombieMan
’s Speed
was 8
). We omit
defining the Spawn
state label since we don’t want to change it. We start
the See
state label by playing the “brain/cube
” sound lump using the
A_StartSound()
function (it’s just the DSBOSCUB
lump from
freedoom2.wad
originally used in Chex’s Quest) with the starting time 60%
into the audio. And then the A_SetRenderStyle()
function is used and the
sprites are set to be rendered in the “shaded” style (just like for
MFM_Elixir
) with the new StencilColor
for 4 ticks. And then the
RenderStyle
gets changed again to “fuzzy” style, which is internally
defined for use with the Spectre
class of enemy from the Doom games.
After 4 more ticks, MFM_InviZombie
should enter our custom See2
state
label, which is just the looped state sequence from the default See
state
label in the original ZombieMan
class. In game, the actor will be visible
normally in the Spawn
state label, but will enter the See
(and
eventually the See2
) state label and look like a dark shade when alerted,
like so:

When in the See2
looped state sequence, the sprites are cycling
between the POSS[ABCD]
walking-animation frames at the speed defined
in the code (look them up using SLADE). Next, let us add a modified
Missile
state label to the States
block of MFM_InviZombie
class:
Missile:
POSS E 1 A_StartSound("misc/spawn");
POSS E 4 A_SetRenderstyle(alpha, STYLE_Shaded);
POSS E 4 A_SetRenderstyle(alpha, STYLE_Normal);
POSS E 12 A_FaceTarget;
POSS F 8 A_PosAttack;
POSS E 8;
Goto See;
If you compare this to the default Missile
state label from the original
ZombieMan
class, you will see that the “misc/spawn
” sound lump is
played (it’s just the DSITMBK
lump from freedoom2.wad
originally used
in Chex’s Quest), the render style is briefly set to “shaded” for 4 ticks,
and then the rendering style is set to “normal”. Also, it now takes
1+4+4+12
meaning 21
ticks to get to the A_PosAttack()
call, which is
longer than the default 10
ticks. This is to keep the monster difficulty
roughly the same as before, giving the player more of a chance to execute a
counter attack. Lastly, the actor is returned to the See
state label
which should play the other sound lump and set the render style back to
shaded (as we’ve already seen). This should look like this:

To finish it up, we can also add new definitions of the Pain
,
Death
, and XDeath
state labels to our replacement class to make
MFM_InviZombie
visible in those states too:
Pain:
POSS G 1 A_StartSound("misc/spawn");
POSS G 4 A_SetRenderstyle(alpha, STYLE_Shaded);
POSS G 4 A_SetRenderstyle(alpha, STYLE_Normal);
POSS G 3 A_Pain;
Goto See;
Death:
POSS H 1 A_StartSound("misc/spawn");
POSS H 4 A_SetRenderstyle(alpha, STYLE_Shaded);
POSS H 5 A_SetRenderstyle(alpha, STYLE_Normal);
POSS I 5 A_Scream;
POSS J 5 A_NoBlocking;
POSS K 5;
POSS L -1 A_DropItem('MFM_Elixir');
Stop;
XDeath:
POSS M 1 A_StartSound("misc/spawn");
POSS M 3 A_SetRenderstyle(alpha, STYLE_Shaded);
POSS M 1 A_SetRenderstyle(alpha, STYLE_Normal);
POSS N 5 A_XScream;
POSS O 5 A_NoBlocking;
POSS PQRST 5;
POSS U -1;
Stop;
You should, once again, compare these to their counterparts in the
original ZombieMan
class. We won’t be redefining the Raise
state
label. I have thrown in the use of the A_DropItem()
function at the
end of the Death
state sequence to make the MFM_InviZombie
drop a
bottle of MFM_Elixir
when killed. This item is dropped along with
the ZombieMan
’s default DropItem
(Clip
). This should make the
difficulty more fair.

I have omitted dropping the MFM_Elixir
from the XDeath
state to
avoid rewarding the act of blowing the monster up with some
splash-damage weapon that didn’t require as much precision, like
shooting an explosive barrel next to the monster (try summoning an
ExplosiveBarrel
via the console next to an unalerted
MFM_InviZombie
and shooting it). From a game-logic-rationalization
point-of-view, the explosion smashed the bottle with the elixir!
I have not justified some of the decisions made here (speed increase,
partial spectre-like visibility, inclusion of sound cues, transitional
blue-ish white shading, longer tick durations, conditional drop items)
as game design is beyond the scope of this guide. But you can vary
these parameters yourself and see what suits your needs best. You can
make the monster completely invisible in any state sequence by
replacing the sprites with TNT1A0
, which is a dummy lump name for a
blank sprite. Better yet, the engine already provides a convenient
flag that you can set to turn any monster into a Stealth Monster. All
you have to do is add the line +STEALTH
to your class’s Default
block. Here is the minimum code you would need:
class MFM_StealthZombie : ZombieMan
{
Default
{
+STEALTH;
RenderStyle "Translucent";
Alpha 0;
}
}
Feel free to summon MFM_StealthZombie
from the console and see how
differently this plays. This flag also has a built-in slow
fade-in/fade-out system so that the transitions aren’t
abrupt. However, we lose some control by doing it this way. For one,
the monster is invisible even in the starting Spawn
state. So there
is no reward for a stealthy player who managed to sneak up on the
monster without alerting it. The timings of all the state sequences
are also the same as the default, and no sound cues have been
added. These types of Stealth Monsters are highly discouraged in the
modding scene as they are very difficult to balance. They get abused
by novice modders and end up becoming very unfair. Game-design sense
is an important skill that, imho, you cannot learn from a guide or
tutorial.
ZScript
is a lot more powerful than what I have presented here.
Everything we have covered could have also been done in the older scripting
language called Decorate
. I didn’t introduce EventHandlers
, custom
functions, dynamic arrays, or
manipulation of level data.
Consider this guide just the start of your journey, and devour the more
advanced guides next: Ash’s ZScript basics and David Newton’s Youtube
Tutorials.
I have introduced the use of the A_StartSound()
function but did not
introduce the SNDINFO
or SNDSEQ
lumps or show you how to include
custom sounds in your mod. I encourage you to research and experiment
with these on your own.
Basics of mapping
Doom modding is dominated by custom maps. This is the most popular format for sharing and enjoying the community’s creations. Over the years, there have been several mapping contests: spanning long-term veteran quality collections (see Doomer Board Projects) to newbie-friendly, all-comers compilations (checkout RAMP, run by David Newton). Single-map mods have won Cacowards quite often. Mapping for Doom can be a very fun, addicting experience. In this guide, I will only cover the basic setup of one of the programs that can be used for it, as well as using custom resources, and some quirks about map-only WAD files and their lump naming and packaging.
Mapping is fundamentally a visual task. You need an editor with a GUI to
even get anywhere. Therefore, the best tutorials are all in video format.
In fact, (semi-)professional doom mappers livestream their process on
streaming websites. If you were to get on a video-sharing site like youtube
and search for something like “doom mapping tutorials,” and filter for
playlists, some of the earliest uploads you will see are from a personality
who calls himself Chubz. He has a couple of series (here and here) on an
editor called Doom Builder
(he seems to have created one for ACS
scripting too). We won’t be using Doom Builder
here, but the one I will
introduce (Ultimate Doom Builder, or UDB for short) shares some genealogy
with Doom Builder
and related editors. Most of those lessons are directly
applicable with very few required changes that you will be able to figure
out on your own.
More modern playlists from lazygamer or raven67854 are more explicitly geared towards UDB and GZDoom, and are an excellent resource. There are even videos by detractors who don’t like the pace and tenor of these playlists, who have their own video take (some strong language in that one). John Romero himself (who was part of the original Doom team in 1993) livestreamed some mapping he did on Sigil 2 (and UDB was good enough for him). Famous mappers like bridgeburner and Dragonfly make spot-tutorials for obscure/esoteric mapping tricks that serve as reference material for even hardcore modders. James Paddock (who made the midi soundtrack for Sigil 2) and our friend David Newton (why isn’t he writing this!!) also have great mapping videos. You should watch those last two links both before and after going through this section of the guide. It will make more sense the second time. I apologize to other famous mappers with a catalog of video tutorials for not mentioning them here.
Getting UDB
Now that we’ve gotten past the hyperlinks to youtube videos and
playlists, let’s get back to some exercises. This being a GUI, this
section will have a lot of screenshots. First, create a directory
named maps
in your mod’s project folder (this is a reserved folder
name). Secondly, to get Ultimate Doom Builder, you can try their
official website, or the ZDoom forums. For Linux, the build
instructions are in the README
file on the github repository
(although on modern Linux systems, install winetricks dotnet472
and
wine-mono
, and use wine
to run the portable archive
Builder.exe). Once you launch UDB, you will be greeted to an opening
screen with some graphic. Using the top-left popup menus, navigate to
Tools -> Game Configurations
(or just press F6
). This will bring
up a Game Configurations
window with a list of map formats on the
left and a bunch of tabs for panels on the right. Select GZDoom:
Doom2 (UDMF)
format from the list, and in the Resources
panel, add
freedoom2.wad
, gzdoom.pk3
, and my_fist_mod.pk3
files to the list
(so that we can use stuff from your mod!). The Game Configurations
window will look something like this:

In the Nodebuilder
tab, you can set both configurations to (do not build
nodes)
if you are on Linux. The engine can build BSP nodes during loading.
In the Testing
tab, you can let UDB know where your GZDoom executable is
if you want to. This is to be able to launch the engine directly from UDB
for rapid testing. This guide won’t be using that method, as it robs us of
the opportunity to learn a few other remaining, beginner concepts.
With the Game Configurations
out of the way, now navigate to File
-> New Map
(or just press Ctrl + N
). It should bring up this
Map Options
window:

Select the GZDoom: Doom 2 (UDMF)
option for Game
configuration
. Leave Script Type
option as ZDoom ACS
(even though
we won’t be bothering with ACS scripting here). Make sure that the
Level Name
option is MAP01
. This is important. This is not the
name of your file. Nor is it the conceptual name of your map. It is an
engine internal identifier lump, and I will explain its significance
further down. You can re-add the three resource files:
freedoom2.wad
, gzdoom.pk3
, and my_first_mod.pk3
here. Hit okay
and you will be greeted to a blank grid, with some unlabeled buttons
to the top and the left (hover over for a tool tip). There will be a
broad, bottom bar that says Vertices Mode
. This can be hidden, but
let’s leave it open since we are a beginner. At the bottom right,
among other things, there will be a grid-spacing setting (I think it
defaults to 32 mp
). The view should look like this:

Your first room
For starters, do not click anything. Save the blank map inside the
maps
subfolder in your mod’s project folder. Give it a name like
myfirstmap.wad
. The top title of the UDB window should now say
“myfirstmap.wad (MAP01)
”. Now let us learn our first hotkeys. Press
the L
-key on your keyboard. The broad, bottom bar should now read
Linedefs Mode
instead of Vertices Mode
. You can get back into
Vertices Mode
by pressing V
. Pressing S
should swap you to
Sectors Mode
, and hitting T
will take you to Things Mode
. Just
practice changing between these various modes. What your left-click or
right-click is able to do depends on what mode you are in. To draw or
select lines, you will have to be in Linedefs Mode
. To draw/select
sectors, be in Sectors Mode
, and so on. You can, of course, switch
between these same modes by clicking on the appropriate button on the
left panel. But I want you to get used to using hotkeys. By the end of
year-two of Doom mapping, I want you to be able to match
Bridgeburner’s hotkey prowess from this video. Write them down in a
cheatsheet if you have to. You can, of course, change the default
hotkeys to your preferred bindings later.
Right. Now let’s focus on the blank grid expanse in the center of the
screen. This grid presents a top-down plan view of your map (which is empty
right now). Here, you can use the mouse’s scroll wheel to zoom in and out.
And if you hold down the spacebar
-key, your mouse movements will cause
the grid to pan. It might be hard to tell with the only fixed-point of
reference being the plus-sign at the center/origin. So let’s create a point
of reference by drawing a rectangular room somewhere. Press Ctrl + Shift +
D
to enter the Draw Rectangle Mode
. Now left click somewhere in the
blank grid a little bit to the top-left of center. Let go of the mouse
button. Now move the mouse pointer towards the bottom-right until the
rectangle being dragged open is a decent size (something over 256 \(\times\)
256 is good). Left click again. This should materialize the rectangle and
kick you out of the Draw Rectangle Mode
.
You are looking at the top-down view of your first room. All you can
see is its floor texture (there is a button that switches it to show
the ceiling texture in the top-down view, but I won’t tell you where
it is!). This room has one Sector
, four Vertices
and four
Lines
. The Lines
have a little notch at the center pointing
inwards into the room. This notch tells you where the “front-side” of
the line is. These four Lines
are single-sided lines since their
backs are facing the void. Now if you switch to Sectors Mode
(S
)
and left-click on the room’s floor anywhere, you will “select” the
sector. Press the C
-key to cancel the selection. Switch to Linedefs
Mode
(L
) and you will be able to select the lines by left-clicking
on them. You can select multiple lines by drag selecting, or clicking
on multiple of them in sequence. Again, hit the C
-key to clear the
selection. The C
-key is your best friend.
When in any of these modes, if you right-click on the appropriate entity
(and not in the void or empty region), you will bring up a window for its
properties. If you had multiple entities selected before you right-clicked
on one of them, the properties will collectively apply to all of them. If
you accidentally right-click in the void (or within a Sector
while in
Linedefs Mode
), you will start drawing lines. Hit the Escape
-key to
snap out of it. The Escape
-key is your other best friend. If you
right-click on a Line
while in Vertices Mode
you will create a new
Vertex
and split the Line
into two Lines
there. Hit Ctrl + Z
to
undo that action. Or select that Vertex
while in Vertices Mode
and
Delete
it.
Using custom textures from your mod
Now, get into Linedefs Mode
(L
) and right-click on the top (North)
Line
of your room. It should bring of a properties window with tabs
in a top row. Click on the Front
tab (remember, this line is
one-sided and doesn’t really have a Back
-side). To the right you
will see a column of three gray squares, with the middle square
displaying a texture and a name like STARTAN1
or STARTAN2
(these
are just the UDB defaults). Left-click on the middle square to bring
up another window that allows you to select some other texture. It
will show you all the available textures to choose from based on the
resource files (freedoom2.wad
, gzdoom.pk3
, and my_first_mod.pk3
)
that you specified in the Game Configuration
setting.

Scroll down until you see my_first_mod.pk3
in the list. Selecting it
and then All
will bring up three textures. If you recall, I used a
custom image (of John Romero’s forehead) called John_Romero.png
and
then created a virtual texture lump in the TEXTURES.lmp
patch called
BIGDOOR1
. The both of these, as well as our modified AQRUST08
texture are showing up as selectable options. Note that only the
first-eight DOS-friendly characters of John_Romero.png
show up. Let
us select our modified AQRUST08
and apply it to the middle-texture
slot of the Line
. As far as playing is concerned, it doesn’t matter
whether you use the AQRUST08
texture lump from freedoom2.wad
or
my_first_mod.pk3
here, as the mod ensures that the replacement gets
applied in GZDoom. But for UDB to visually display the right one
within its views, we might as well use our modified lump.
There is a checkbox for “Long Texture Names
” in the Browse texture
window. NEVER turn that on. It will store the absolute file-path to
your texture file in the map wad instead of the short-lump name. It is
better to use the lump names, as this keeps your mod … well
… mod-able (both by yourself and others). If someone in the future
makes a replacement mod that, say, replaces AQRUST08
with a
higher-resolution version with PBR-material shaders for realistic
lighting and reflections, that replacement won’t take effect on your
map if it isn’t just using the simple lump name AQRUST08
.
Okay great. We have a room with one wall painted with our modified
texture. To actually make this map playable, we need to add a
player-start Thing
. Enter Things Mode
(T
) and right-click
anywhere inside the room. This should, by default, place a Player 1
start
“Thing
” on the map and bring up its properties window. Here
you can set the starting Angle
you want the player to face and hit
OK
. If you accidentally placed additional Things
, select them in
Things Mode
by left-clicking them and hit Delete
(or use Ctrl +
Z
a bunch to undo recent steps).

Now just an empty room (albeit with some art on one of the walls) is
boring. Let’s add some other Things
for the player to interact
with. While still in Things Mode
, right-click at some corner of the
room and expand the Monsters
folder in the Things
selection
panel. You should be able to select an entry called
$FN_ZOMBIE
. Normally, this would have said “Former Human
”. But
since we added a class-replacement clause in our ZScript
file for
the ZombieMan
class, UDB doesn’t know which resource file to
prioritize. So it is trying to display a Tag Name
, but is running
into a LANGUAGE
lump substitution bug (don’t worry about it, it
might have gotten fixed by the time you read this). Anyway, select
$FN_ZOMBIE
and make sure that his Angle
is facing away from the
Player 1 start
Thing
so that he doesn’t get alerted as soon as the
game starts. When you launch the game with your mod, the class
replacement will automatically kick in, and you will be fighting an
MFM_InviZombie
. Similarly, scroll down to find the Health Bonus
Thing
and place it somewhere else in the room.

Placing custom things from your mod
Now you might be wonder, how do we place our shiny MFM_Elixir
Thing
into the map. Even though we defined it, we removed the
replacement clause (like I asked you to!). So it exists as a separate
independent entity that can be summoned in the game from the console
(and by killing the MFM_InviZombie
class with weak bullets). But it
is not available in this list for pre-placement on the map. To fix
this, we have to give it an Editor Number. Leave UDB open in the
background (minimize it or switch to a different workspace if your OS
allows it). Now create a new file in your mod’s project folder called
MAPINFO.lmp
. In it, put the following text:
DoomEdNums
{
20001 = "MFM_Elixir"
}
Map wad files aren’t storing Things
in them by their entire
definition. It only stores a Thing
’s editor number (along with some
map properties like starting angle). Most standard Things
have a
standard editor number that all Doom source ports and map editors
respect (more or less). For GZDoom, as of this writing, all editor
numbers between 11,000 to 14,000, and between 14,166 to 31,999 are
available to modders for custom classes. Here, we have assigned the
number 20001
to our custom MFM_Elixir
class in a new (reserved
name) lump file MAPINFO.lmp
. Now re-zip the archive:
zip -r my_first_mod.pk3 textures/ TEXTURES.lmp sprites/ ZSCRIPT.zsc MAPINFO.lmp
I understand that this zip command is getting a bit long. I will
present my solution to this problem later. For now, it is important to
know what is (and what is not) going into the pk3
. For example, I am
still leaving out the maps
folder that I had you create a while back
in this section. Also, since we are editing these files, the editing
software can sometimes create temporary or permanent backup copies of
the files (usually with the same names and a tilde “~” character
appended in the end). Make sure that you don’t accidentally zip those
into the pk3
file.
Now return to UDB, navigate to the Tools
popup menu and reload resources
(F8
-key) from your resource files (freedoom2.wad
, etc.). Right-clicking
in Things Mode
again should allow you to scroll down to the
User-defined
folder and place the MFM_Elixir
class in your map. The
display sprite for it in UDB will be the same as that of the Health Bonus
since we didn’t actually change its Spawn
state sprite. We merely changed
the rendering style, which is an engine thing, not a UDB thing.

Visual mode and Line
action special
The room is going to be fun for a while. But eventually, the player will
want to leave it. Let’s add an exit button. Switch to Vertices Mode
(V
). Right click at two places on the northern Line
of the room again
to place two Vertices
exactly 64 map-units apart (it should be easy if
the grid-spacing is set to 32 mp
at the bottom right). This splits the
northern Line
into three. Switch to Linedefs Mode
(L
) and right-click
on the middle Line
as shown to bring up its properties window. In the
Front
tab again, change the middle texture to a switch texture like
SW1COMP
(you can type it below the gray square without bringing up the
Browse texture
window). Any switch texture that is 64 units wide or
greater is good here. Hit OK
.

The switch is just a texture painted on the wall. You can’t activate
it yet. Before we learn to make it do that, I want to introduce you to
the visual mode in UDB. So far, we have been looking at our level in
the top-down “plan” view. Here, we could switch amongst a bunch of
“modes” and operate on the map. But you can look at your map in a 3D
perspective from within UDB. Move your mouse cursor somewhere close to
or inside the room, and hit the Q
-key. Now look around using the
mouse. Do you see your room? Don’t click on anything. Hit Q
again to
return to the “plan” view. Move the mouse cursor over to a different
location and enter visual mode again (Q
) to spawn your viewpoint at
that new location.
In visual mode, you can actually fly the viewpoint through the map
using the E-S-D-F
keys by default (these are shifted by one key over
to the right from the standard W-A-S-D
movement keys for FPS
games). While in visual mode, the “modes” have no meaning. The
map-entity (walls, floors, ceilings, or Things
) that the central
cursor is pointing at will be highlighted with a pulsating orange
glow, unless highlighting has been toggled off with the
H
-key. Anything you do (like hit keys or scroll the mouse wheel)
will affect the entity highlighted, even if the highlighting effect
has been visually turned off (hit H
to turn it back on again). Like
pressing the arrow-keys (with or without the Shift
-key pressed)
while a wall or floor is highlighted will offset its texture. The
arrow-keys can move locations of highlighted Things
. And the mouse
wheel can raise or lower a highlighted floor or ceiling. People,
epecially newcomers, find this behavior very frustrating. The way to
avoid this problem is to actually select the entities you want to
manipulate by left-clicking them (yes, while in visual mode). Selected
objects pulsate in red, and you can selected multiple entities by
sequential clicking on them (press your best friend the C
-key to
clear selection). When one or more entities are selected, hitting keys
or scrolling the mouse wheel will now only manipulate the selected
entities. Let’s practice doing it this way.

Find the middle-segment of the northern wall with the switch
texture. Unless you really lucked out with your room Vertices
location, it will most likely be misaligned as shown. Left-click on it
while still in visual mode so that it pulsates with a red highlight
instead of orange. Now use Shift
plus the left or right arrow keys
to change the texture offsets until you are satisfied with the
alignment. Hit C
to clear the selection and then Q
to exit back to
the “plan” view mode.
Now, to make the switch actually usable, we have to give that middle
Line
an action special. From Linedefs Mode
, right-click on that
line, and in the Properties
tab, in the Action
panel, you can
click on the list icon to the right of the wide bar and search for the
End Normal
action special (or just type the number 243
in the
smaller leftward box as shown). Then in the Activation
panel, click
on the “When player presses use
” checkbox to tick it on. Hit
OK
. Now your Line
will execute the action special End Normal
defined in the UDMF
map standard when the player presses the USE
key while facing it (and is close enough). Note that we assigned this
to the Line
, and not the switch texture. That whole wall is
technically the switch, and can be activated from any height (like
from a ledge or ladder). That picture of that switch at the bottom is
just for show. This is just how Doom works. If you want a height
restriction, you will have to do something clever like a lowered
ceiling and a recessed sector.

Save your map (Ctrl + S
). Now to play it, I already committed to not
showing you how to do it from within UDB. So we use the launcher
program and add the map file myfirstmap.wad
(it was saved in the
maps
subfolder of your mod’s project folder) to the list of mods in
our preset. Then launch the game. You can (in my experience) do this
while the map file is open in UDB. If you want to launch from a
terminal command prompt instead, the command would be:
./gzdoom -iwad freedoom2.wad -file <PATH-TO-PROJECT-FOLDER>/my_first_mod.pk3 <PATH-TO-PROJECT-FOLDER>/maps/myfirstmap.wad
Voila. You are playing your map in the game. There is the
MFM_Elixir
, and the MFM_InviZombie
. And that tempting switch. If
you rush forward and hit it, the level MAP01
will end and the game
will show you the intermission screen, and then load MAP02
for you.
Switch patch and animation
You might have noticed that when you hit the level-exit switch, the level just ends. The switch didn’t actually visually flip, and there was no switching sound made. While this is okay for a level-exit switch (after all, there is no time for the sound to play), it will become problematic if you want to use that switch texture for other things (like opening doors) in other places on the map. Side-note: Typically, you are supposed to have a distinctive looking special switch for level exits that don’t look like other switches that do other things. A game-design aspect of visual clarity. But let’s leave that aside for now.
When I introduced the TEXTURES
lump, I promised to show you how to
slap a switch onto any wall texture. The switch textures on offer from
freedoom2.wad
(browse through them through UDB now instead of SLADE)
all come with background wall textures, and none of them are on
AQRUST08
. Let’s fix that. In our TEXTURES.lmp
file, we can define
two new texture lumps by combining our modified AQRUST08.png
file
and two internal switch lumps called SW1S0
and SW1S1
from inside
freedoom2.wad
like so:
Texture MYSW1, 64, 128
{
Patch "textures/AQRUST08.png", 0, 0
Patch SW1S0, 16, 82
}
Texture MYSW1ON, 64, 128
{
Patch "textures/AQRUST08.png", 0, 0
Patch SW1S1, 16, 82
}
I have called these new virtual texture lumps MYSW1
and MYSW1ON
,
and their overall sizes are 64 \(\times\) 128 pixels. The patches are
applied in the order they are specified, and I have carefully chosen
the patch offsets to put the switch at the bottom center. Now, we
create a new file with yet another reserved lump name: ANIMDEFS.lmp
(be sure to not type “anime” by mistake). Its contents should be:
switch MYSW1 on sound Switch1 pic MYSW1ON tics 0
switch MYSW1ON off sound Switch2 pic MYSW1 tics 0
This is invoking internal sound lumps named Switch1
and Switch2
from
inside freedoom2.wad
, and telling the engine to treat our two new virtual
textures as switches that are complementary states of each other. Now
zip-up the whole thing again:
zip -r my_first_mod.pk3 textures/ TEXTURES.lmp sprites/ ZSCRIPT.zsc MAPINFO.lmp ANIMDEFS.lmp
In UDB, you can reload the resource files (F8
) and change that one
Line
’s texture to the newly available MYSW1
. Go into the visual
mode and align the texture of that middle wall (and/or neighboring
walls). Come out of visual mode. Save (Ctrl + S
). Now launch the
game again with your map. This time, assuming that your framerate is
high enough, you should see the switch change states (and briefly hear
the Switch1
sound effect) as the level ends.

Practice a small set of hotkeys over and over again until you get the hang of them. Then add one new hotkey for every new feature you learn. To review, watch this. And this and this.
The Mapfile lump
You can close UDB now. We are done with that.
Let us analyze what happened when you launched the game with
myfirstmap.wad
. You might recall that when you first started a new map in
UDB, it opened a Map Options
window where there was an option called
Level Name
. I had asked you to leave this as the default MAP01
. Well
the IWAD that you feed the engine contains instructions for what map to
start the game in, and which map follows which. freedoom2.wad
, for
example, instructs the engine that a map with the lump name MAP01
is
supposed to be the first map, and that MAP02
should be the next one when,
for example, the End Normal
action special is executed from MAP01
. And
this is why, when you loaded you map with the freedoom2.wad
as the IWAD,
it started you off in your custom map. And upon hitting the exit switch, it
sends you off to MAP02
(contained within freedoom2.wad
). Your map
replaced the default MAP01
lump inside the IWAD.
Changing the map lump name
Now let’s do some experiments. Open the launcher program again, and
this time launch your mod with freedoom1.wad
as the IWAD (I hope you
didn’t delete that one after the download!). You will note that the
game does not start you off on your custom map. It just starts you off
in Freedoom 1’s regular maps, no matter which of the four “Episodes”
you start with. In fact, you can play through the whole of Freedoom 1
and never encounter your map.
That is because Freedoom 1 instructs the engine to follow a different
set of map lump naming conventions. If you start with Episode 1, the
engine starts you off in map lump E1M1
. The next map in the sequence
is called E1M2
. And so on. You can still tell the engine to force
you into the map though. Launch with freedoom1.wad
as the IWAD
again, and this time, run `changemap MAP01` in the console. If you hit
the level exit switch though, it will take you to the end credits
sequence, since the IWAD freedoom1.wad
doesn’t tell the engine what
the next lump after MAP01
should be.
You can define your own sequence of maps and map lumps in your
MAPINFO.lmp
file. This is also where you would specify the level
soundtracks, what sky texture to use for each level, give them fancy
nicknames like “Hanger Base”, and so on. I won’t show you how, but
know that this exists. And it doesn’t have to be a linear sequence of
maps (with or without episodes) either. You can even specify clusters,
with a central hub map and several “spoke” maps that you can travel to
and return from. Godless Night is my favorite example of this
structure. We will see an example PWAD that does this later.
Open myfirstmap.wad
in SLADE. It can do it. It is a WAD file after
all. You should see its insides:

It is simple, as it should be. Maps can get a little heavier that that
later on, when you add more complicated things to your map, like ACS
scripts, and Dialogue lumps. Remember that rule I had about not
modifying anything through SLADE? This is where we break that
rule. Rename the first marker lump from MAP01
to MAP02
. Save. Do
not change the actual WAD file name. Now launch the game again with
freedoom2.wad
. This time, you will have to play through the entire
first level of the base game before it plops you into your custom map,
which now has the lump name MAP02
. And after you hit the exit switch
in that, the engine will load MAP03
for you. You could have named it
E1M1
and launched it with freedoom1.wad
as well. Now change the
name back to MAP01
again through SLADE (hit save before closing).
Packing the map with your mod
So far we have some texture/sprite replacements, and custom classes
mod in a pk3
file (my_first_mod.pk3
), and a map in a wad
file
(myfirstmap.wad
). But can we smoosh them together? The pk3
format
already has a way to do that. First, rename myfirstmap.wad
to
map01.wad
(the file name has to match the lump name for this
method). Then, just add the map file (along with the maps
subfolder)
to the zip archive:
zip -r my_first_mod.pk3 textures/ TEXTURES.lmp sprites/ ZSCRIPT.zsc MAPINFO.lmp ANIMDEFS.lmp maps/map01.wad
And you are done. Note that I specified the file name maps/map01.wad
in
full in the command instead of just the subfolder name maps/
. This is
because if you look inside the subfolder, you will see a lot of backup
files that got created by UDB and SLADE when we opened the file in them. We
don’t want these files ending up in our pk3
file. I could have specified
a regular expression like maps/*.wad
too to catch only the files that end
in a .wad
. In the end, the structure of your full my_first_mod.pk3
should be:

Now if you launch the game with just freedoom2.wad
(or Doom2.wad
) as
the IWAD, and your mod my_first_mod.pk3
as the PWAD. No extra map file
needed. You should be playing your mod in your map. You can create more
maps, name them MAP02
, MAP03
, and so on, and package them all inside
the maps
subfolder in your pk3
file. There is some more work to be done
if you want to make your own standalone IWAD. We will analyze some publicly
available IWADs in SLADE later. But for all intents and purposes, you are
now a certified doom modder. Now go create something!
Misc.
Studying some public mods
We have barely brushed the surface of most concepts in this guide. So our knowledge is limited when it comes to analyzing extant mods. But there are plenty of popular but simple mods out there that we can look into, and much can be gleaned from even the complicated ones using some guesswork and context clues. Let us download the following mods in order:
OTEX texture pack
The OTEX textures are a wildly popular option for modern megawads and
mappacks. They are created by the veteran texture artist Ola “Ukiro”
Björling, who has also made some famous Doom maps back in the day, and
dabbles in music. If you’ve played through either of the Eviternity
megawads, you’ve seen these textures. You can get them from this
forum post on Doomworld. It links to a downloads’ page on Ukiro’s on
website, wherein you can read his permissions text, and download links
to both the *.wad
and *.pk3
versions. Download the pk3
file and
open it in SLADE. You should see this simple file structure inside:

All three folder names are reserved names, as is ANIMDEFS
. The
README.txt
file contains the credits list, the redistribution
permissions, a changelog, and some useful information about creating
new textures using the png images inside the patches
folder. You
would do what you learnt regarding the TEXTURES
lump in making the
level-exit switch for you first map. Ukiro has been kind enough to not
just share finished textures, but to make the raw patches public to
allow anyone to mix and mash them into new textures.
The ANIMDEFS
lump contains a short list of switches, just like we
defined in the mapping segment for the level exit. Further down, it
contains a bunch of texture sequences meant for animations:

For example, the first line is instructing the engine to animate the
texture lumps OROCKO2A
, OROCKO2B
, OROCKO2C
, and OROCKO2D
by
sequentially replacement every 4
ticks (remember that there are 35
ticks per second, see the wiki for the syntax). You can find these textures
in the Textures
folder and cycle through them to see how they animate.
They constitute a pulsing, glowing, solidified lava surface. Further down
in ANIMDEFS.txt
you will find the line:
Flat Optional 0ICYWA01 Range 0ICYWA08 Tics 5
The texture lumps 0ICYWA0x
(x
goes from 1
through 8
) simulate an
icy water surface (you can find these in the Flats
folder). This is a
great way to animate liquid-surface textures like water, nukage, lava,
blood, or sewage goop. Every frame of these liquid textures is designed to
tile seamlessly with themselves, so you can cover arbitrary shapes and
sizes of contiguous areas with them.
If you are getting into mapping, adding the OTEX pk3
as a staple
resource file (along with the UDB config on Ukiro’s website that helps
arrange the textures into subcategories) will offer a great many
inspiring set of textures to play with. Just be sure to include the
pk3
file in your launcher too!
IDKFA music wad
The IDKFA album (named after one of the cheat codes from the original
Doom release) is a modern, metal reimagining of Bobby Prince’s
original Doom 1 soundtrack. It was created by Andrew Hulshult, who has
a lot of music credits in commercial games you may have heard of. The
zip-download on the modDB page is around 720 MB large, but it includes
the music in *.flac
and *.mp3
format for your enjoyment outside of
the engine. The actual IDKFAv2.wad
file within is only 87 MB
large. If you open this in SLADE, you will only find sound lumps with
very specific names. These are the default soundtrack lump names from
Doom 1 (and not Doom 2), and this is a simple lump-replacement wad. In
case you don’t have Doom 1, you can play this with freedoom1.wad
too, since the whole point of the FreeDoom
project is to use all the
same lump names.
If you open gzdoom.pk3
in SLADE and navigate to
mapinfo->doom1.txt
, below the GameInfo
block and the episode
definitions (Doom 1 and Freedoom 1 both arrange their levels into
episodes), you will see the map definitions like so:

The music for map E1M1
is defined as the variable $MUSIC_E1M1
,
which is in turn defined in the language.def
lump in the root
directory. As a homework assignment, look up both mapinfo->doom2.txt
and the language.def
file for what to rename the first soundtrack in
IDKFAv2.wad
so that it plays on MAP01
in Doom 2 or Freedoom 2,
even with your my_first_mod.pk3
(don’t forget the “D_
” prefix).
Bow and arrow weapon
Next we will look at a small weapon mod by Gothic. This pk3
file can
be download from Realm667.com
, which is a long-running repository of
sorts for community creations. Before opening the mod in SLADE, I
suggest just playing it in Freedoom 1 or 2 to get an idea of what is
being implemented. Put the pk3
in you launcher mod list and start
the game. Open the console and run summon bow
. It should spawn a bow
with ten arrows that you can collect (sits in weapon-slot 3). Now, if
you press-and-hold the primary fire button, the bow gets drawn and
remains drawn. If you let go, the arrow flies (and sticks to the wall
for some time before disappearing). Run give arrowammo
or summon
arrowammo
in the console if you run out of arrows. If you
press-and-hold the alt-fire button (typically bound to the mouse
right-click button), your screen gets an overlay like so:

Using the W-A-S-D
movement keys while the alt-fire button is still
pressed lets you select between four types of arrows. Letting go of
the button will play a weapon-sprite animation and change your arrow
type. The fire arrows explode, and the frost arrow freezes its
target. You can then walk up to it and punch it to pieces!
This is obviously a fairly complex weapon. Analyzing the code for it will
also be difficult with our current skill level. Luckily, Gothic has also
included code for a simplified version of the weapon called BowSimple
.
You can summon this in the game by running give bowsimple
. This gives you
a simplified bow with no alt-fire magic abilities. This one hasn’t been
bound to a weapon slot so don’t deselect the weapon!
Now we can open Bow.pk3
in SLADE. The file structure should look
like so:

The big thing to point out here is the lump named DECORATE.txt
. This is
the older scripting language that ZScript
has since superseded. You
should learn and stick to ZScript
, since it is more powerful and will see
continuous development. But some of the older mods still have a lot of
DECORATE
code in them, and it is good to know how to look at it and parse
it well enough. If you look at the contents of the DECORATE.txt
file, you
will see a definition for the Bow
class at the top. But the syntax is
very different from ZScript
! The class-definition line is different,
there is no Default
block, and where are the semicolons!!
Let us skip past the Bow
class and scroll down to line 212. Here is
the code:
////Simplified Bow, if you just want a regular bow
Actor BowSimple : Weapon
{
Inventory.PickUpMessage "Bow"
Weapon.AmmoUse 1
Weapon.AmmoGive 10
Weapon.AmmoType "ArrowAmmo"
Tag "Bow"
//+WEAPON.NOALERT
States
{
Spawn:
BWPU A -1
Stop
Select:
TNT1 A 0 A_ZoomFactor(1.0,ZOOM_INSTANT)
DBOW A 1 A_Raise
Loop
Deselect:
TNT1 A 0 A_ZoomFactor(1.0,ZOOM_INSTANT)
DBOW A 1 A_Lower
Loop
Ready:
DBOW A 1 A_WeaponReady
Loop
Fire:
DBOW B 4
DBOW C 4 A_StartSound("Bow/Hold",CHAN_WEAPON,CHANF_OVERLAP)
Goto Hold
Hold:
DBOW D 1
{
if(GetPlayerInput(INPUT_BUTTONS) & (BT_ZOOM)) { A_ZoomFactor(4.0); }
else { A_ZoomFactor(1.0); }
}
TNT1 A 0 A_Refire
DBOW E 3 { A_ZoomFactor(1.0); A_Overlay(7,""); A_StartSound("Bow/Shoot",CHAN_WEAPON); A_FireProjectile("ArrowProj",0,true); }
DBOW FBA 3
Goto Ready
}
}
The state block is using some standard state labels for weapons:
Spawn
, Select
, Deselect
, Ready
, Fire
, and Hold
. You can
guess what these standard states correspond to by their names (you can
look up the sprites being used for some contextual help). The Fire
state sequence is obviously entered when the fire button is
pressed. The weapon then runs through the DBOWB0
and DBOWC0
sprites for 4-ticks each and plays the “Bow/Hold
” sound lump, and
transitions to the Hold
state sequence. Here, there is an additional
test to check if the zoom-in button is pressed (you can bind this to
something like the Z
-key from the options menu). And then there is a
call to A_Refire()
, a built-in action function that checks if the
fire button is still being pressed, and returns the actor to the start
of the Hold
state sequence if so. This is the loop the weapon gets
stuck in (with the bow drawn, held in DBOWD0
sprite) for as long as
the fire button is pressed-and-held. But once it is released, the
A_Refire
check will fall through to the next line, where a sound
lump (“Bow/Shoot
”) is played and A_FireProjectile()
is called with
the projectile type “ArrowProj
” (defined further down in the file,
from line 255 onwards). The weapon turns through a three-sprite-frame
run-down animation and returns to the Ready
state sequence.
If you move the TNT1 A 0 A_Refire
line to the end of the Hold
state
block just above the Goto Ready
line, then the BowSimple
weapon will
lose its press-and-hold-fire-to-keep-bow-drawn property and instead fire a
rapid succession of arrows (you can increase the number of ticks for one of
the earlier sprite frames in the Hold
block to drop the fire rate). You
can even replace the projectile type to one of the predefined ones in
gzdoom.pk3
, like PlasmaBall
, to have the bow fire that. A good exercise
might be to move both the BowSimple
and ArrowProj
class definitions to
your ZScript
lump in my_first_mod.pk3
and convert the syntax over to
ZScript
to make it work with just your mod. You’ll need to modify the
starting class-definition lines, add Default
blocks, and throw in a whole
bunch of semicolons. The GetPlayerInput(INPUT_BUTTONS) & (BT_ZOOM)
check
is also done differently in ZScript
(player.cmd.buttons & BT_ZOOM
). You
could even add a slot number to the weapon in its Default
block. Don’t
forget to copy the sprites and sounds over (and credit Gothic). Note the
two new (to you) built-in class types: Weapon
and Projectile
.
I had you skip the regular magic Bow
class definition at the
beginning of the file because it is more complicated. Feel free to
glance at it. It has a whole bunch of non-standard custom state
labels. And it uses a lot of dummy inventory items as tokens to keep
track of what magic-arrow “mode” you are in, and do the conditional
thing. DECORATE
was really limited when it came to custom variables
and functions, so giving and taking inventory tokens (and checking for
their presence/absence) used to be the only way to institute
conditional logic back in the day. We can look at a slightly simpler
example of that concept in this next mod.
Fast-travel through dialogue lump
Now for some shameless self-promotion: we can analyze one of my own
template mods. You can read my description of it from the forum post,
but it is better to just play it (with freedoom2.wad
). There is a
3D-model canoe in the starting level (in every level really) that you
can “talk” to by pressing the “use” key in front of it. If you haven’t
collected the world map (or the partial world map), it won’t give you
any options. But if you had either of the maps, it should let you
travel to any of the other levels shown in the map. I made four small
template maps out of OTEX textures, and threw in some public-domain
ambient soundscapes to sell the location.

I’ve hijacked the built-in character conversation/dialogue system
(originally invented for Strife) that allows you to converse through
dialogue trees with any actor. The maps have some interesting things too.
If you open them in UDB and right-click on the lines at the edge of the
sea, you’ll see the Line Horizon
property being used, making the horizon
look infinitely far away. Some other lines have the Block players
flag
on. Additionally, the sectors representing the floating ice sheets have the
floor waggle function being called on them via ACS scripting (something we
didn’t go over in this guide). And lastly, all the levels are setup to be
in a hub cluster. This means that changes made to the level are persistent
even if you leave them and return, even through saving and loading. You can
test this by destroying the explosive barrels placed in some levels or
writing your name in charred decals on the wall (use give plasmarifle
and
give cell
in the console).
Let’s open the pk3
in SLADE and look inside. The file structure
looks like so:

First, look into the mapinfo.lmp
file. The block
clusterdef 1
{
hub
}
declares the cluster “1
” as a “hub” type. The four map blocks
further below have all been assigned cluster = 1
. The map blocks
also have a “next =
” line that has some other map assigned to
each. I am not really using this feature, but this dictates the next
map that the game would have loaded had I executed a line special like
End Normal
, just like you learned to do in the Basics of mapping
section.
Next, look at the zscript.zc
file in the root directory. It has the
version
string and two #include
lines for two other ZScript
files in
some subfolders: zscript/fasttravelzc/CustomActors.zc
and
zscript/fasttravelzc/EventHandlers.zc
. This is a great way to organize
ZScript
code into separate files instead of dumping it all into a single
lump. I won’t go over the EventHandlers.zc
file (it contains some code
that dictates when, which, and how to draw the world-map images to the
screen). Let’s look at the contents of CustomActors.zc
. It opens with a
class definition called Canoe
of the type Actor
. Within this, besides
the Default
block and the States
block, there is a block we haven’t
seen before:
override void Tick()
{
Super.Tick();
Vel.Z = 0.03*Sin(10*Level.Maptime);
}
This is “overriding” a virtual function called Tick()
that was
inherited from the parent class (Actor
). This Tick()
function gets
called every tick by the engine (35 times a second). Inside it, I am
running Super.Tick()
, which basically runs the Tick()
function
from the parent, just to make sure nothing important gets missed. Then
I am modulating the vertical velocity of the Canoe
sinusoidally to
simulate the boat bobbing in the water. I could have gotten some
default bobbing behavior by flipping the “+FLOATBOB
” flag in the
Default
block, but this approach gave me more control.
Further down the file, starting from line 29, I define a class named
WorldMaps
of type Inventory
. It has some lengthy definition that we can
skip past. Starting from line 91 onwards, I’ve defined two classes named
WorldMapPartial
and WorldMapFull
, both of type WorldMaps
(meaning
they inherit all that lengthy definition that we skipped analyzing). They
both sport a couple of custom Default
block lines, and a couple of
overrides for standard Inventory
item functions. The HandlePickup()
overrides make sure that WorldMapFull
replaces WorldMapPartial
in the
inventory when picked up, and that WorldMapPartial
cannot be picked up if
the toucher
already possesses an instance of WorldMapFull
. The
TryPickup()
overrides are actually giving the toucher
additional
inventory items. The WorldMapPartial
comes packaged with a FortBluePass
item and a BrickPortPass
item. WorldMapFull
also awards those, along
with an additional WasteProcPass
item, and an ArcticBasePass
item.
You might have noticed that these items bare the short-hand names of
the four levels in this hub. Lines 145-170 show that these are dummy
inventory tokens of a custom type: “Passport
”. These are used by the
canoe
actor in deciding what options to make available to the player
based on what passports the player has in their possession. More on
that later.
Line 172 starts a lengthy definitions for yet another custom
Inventory
type class named FastTravelTicket
. The code handles a
screen fade on a timer, followed by a call to
level.ChangeLevel()
. Below this are four quick tickets defined (one
for each level), all of type FastTravelTicket
(and thus, inheriting
its parent-class properties). These inventory items will begin a
level-transition sequence as soon as they enter the player’s
inventory, and destroy themselves once the transition is
triggered. You can test this in the game. In the opening area, run
give BrickPortTicket
in the console to immediately begin
transitioning into MAP02. These inventory items can even be
placed/summoned into the map. They will be invisible since their
Spawn
state labels lack a sprite assignment, but if you run summon
WasteProcTicket
instead of give WasteProcTicket
and then walk
forward, you will “pick” the ticket up and transition to MAP03.
Lastly, let’s address the conversation/dialogue system. The detailed
wiki article is pretty good, but the video tutorial by Chubz is more
instructive. Towards the end, he introduces a shopkeeper mechanic and
shows how the player’s inventory can be scanned for conditions
(presence of a particular gun type or certain amount of currency) and
conditionally display specific dialogue options based off of that. I
have exploited this mechanic to check for possession of world map
items (and passports), and conditionally display options to go to
specific levels. You can look at the dialogue lump within, say,
MAP01.wad
in this pk3
by either opening it in UDB, or
double-clicking on MAP01.wad
in SLADE and having it open in a
separate tab like so:

The Universal Strife Dialogue Format has its own unique syntax, but
the terms use words from natural language in an intuitive
manner. Terms such as link
, ifitem
, choice
, require
, and
giveitem
explain themselves.
MyStandaloneGame template
To close this section off, let’s cover at least one IWAD (*.ipk3
). Nash
Muhandes is part of the engine Dev team, and has a big catalog of GZDoom
projects. His MyStandaloneGame became the base template for a multitude of
projects (it even got linked to on the wiki). It is launched by itself with
GZDoom (meaning not with Freedoom 2 or Doom 2). Meaning:
./gzdoom -iwad MyStandaloneGame.ipk3
Judging by the version number in line 1 of the zscript.zc
lump, this was
originally created for GZDoom version 3.3.2. So launching it in modern
GZDoom will throw out some warnings about the defcvars.txt
lump trying to
set some engine CVars
that no longer exist. But the game will still
launch. Among the new lumps in the root directory, the iwadinfo.lmp
and
the playpal.lmp
lumps are the necessary ones for the engine to be able to
recognize it as an IWAD. The playpal
reserved name is meant for a palette
lump. Since we have been using truecolor png
files for textures and
sprites, the playpal.lmp
file can just be a dummy placeholder.
Inside the file
zscript->MyStandaloneGamePlayer->MyStandaloneGamePlayer.zc
is the
definition of a class named MyStandaloneGamePlayer
of type
PlayerPawn
. This is a special class for the actor that the player
controls, and gets really complicated the more you dig into it. Nash
has used his minimalist sprites to populate a whole bunch of standard
state labels, which the engine will automatically transition the
player into when certain conditions are met. And then the line in
mapinfo.lmp
that goes
PlayerClasses = "MyStandaloneGamePlayer"
assigns this PlayerPawn
class to your player in this IWAD. You can
actually play through MAP01 of Freedoom 2 as this player by running:
./gzdoom -iwad MyStandaloneGame.ipk3 -file freedoom2.wad
where freedoom2.wad
is being interpreted as a PWAD despite being an IWAD.
What happens when you get to the end of MAP01 and hit the exit switch? Can
you think of the smallest change necessary to mapinfo.lmp
that would let
you transition from MAP01 to MAP02 of freedoom2.wad
on level exit,
instead of going to the end-credits? Try adding Next = "MAP02"
(with the
quotes) to the MAP
block.
Try running:
./gzdoom -iwad MyStandaloneGame.ipk3 -file freedoom1.wad
Why didn’t the game start you in the starting level of Freedoom 1? Try
running changemap E1M1
in the console. Does that reveal the problem?
Now what about treating this ipk3
as a PWAD?
./gzdoom -iwad freedoom2.wad -file MyStandaloneGame.ipk3
What does the above command do? Or how about:
./gzdoom -iwad freedoom1.wad -file MyStandaloneGame.ipk3 -file freedoom2.wad
Watch out! The enemies and pickups now exist but your PlayerPawn
doesn’t start with a weapon. Quick, run give shotgun
in the console!
Productivity boosting microhabits
Anything good takes a long time to produce. So it necessarily has to take several sessions, perhaps over months/years. We have this romanticized idea of the crazed artist being struck by inspiration and then running to the basement and etching a piece or composing a poem in one sitting. While art that drains the artist has its place, I recommend the more structured approach of habit formation. This will let you sustain hobbies for much longer and actually stick with projects until the finish line without risking burnout. It is good to have a set time table for modding, and it is actually a good thing to stop working when you are “hot.” It makes the first few things to do in the next session very obvious, allowing you to hit the ground running and not waste cognitive energy on pointless decision-making.
Workstation setup and IDEs/text-editors
Much like in gaming, working consists of several nested state-loops operating over various timescales. And tiny discomforts or frustrations in the smallest loop can snowball over time into unpleasantness, and can cause us to not want to start/continue working without realizing why. It is important for the steps that are the smallest but are also repeated the most often to be as stress-free as possible. All micro-stressors should be eliminated. Just arranging your dedicated workstation at a comfortable desk and chair, and mounting the screen at mid-eye level while maintaining good posture can have a huge impact on your productivity. Have drinking water available on a different and/or lower platform to avoid spilling. If making/editing maps, drop the in-game music volume to zero and play some instrumental soundtracks in the background on your computer, so that it keeps playing constantly as you go back-and-forth between UDB and the engine. Reduced brightness and a low contrast between background and text also goes a long way when working with text.
Almost never having to move your arms maintains a low-latency between the
act of desiring something of the computer and having it fullfilled. So
avoid moving one hand to the mouse and back, or scanning and navigating
popup menus, whenever possible. The OS-specific keyboard shortcuts for
switching between windows (typically Alt + Tab
and Shift + Alt + Tab
)
or switching workspaces (Ctrl + Alt + Arrow Keys
) are a must if you are
new to them. Much like web browsers, certain terminal programs support
multiple tabs within the same window, which can be switched between with,
typically, Alt + Number Key
. Most terminals and command prompt programs
also remember the history of commands, which can be revisited or cycled
through by hitting the UP-Arrrow Key
(or Ctrl + P
if you are on Linux).
Similarly, I use keyboard shortcuts to (a) move the cursor to the start/end
of the line, (b) move cursor forward/backward by whole words, (c) delete
rest of the word/command to the right of the cursor, (d) delete one word at
a time, (c) hit Tab
a bunch of times after having typed a “word”
partially to either autocomplete it, or list available valid
autocompletions. That Tab
-completion trick is great for file-system
navigation and typing out long file names. So I never find typing out
commands frustrating. I can revisit a previously executed malformed
command, navigate to the mistake, fix it and re-execute it with very few
key presses without moving either of my wrists.
If you are new to coding, I suggest looking into IDEs. ZScript
is
not a complicated language, and you will never generate too many
libraries and #include
clauses but a good IDE can still be
helpful. It’ll give you syntax highlighting, auto-indentation, code
folding and fast intra- and inter-file navigation, and popup hints for
function arguments/definitions and possible completions as you type
them. Power-coders in the community swear by VSCode and have some
extant packages available to configure it for ZScript
. Someone even
made a ZScript
syntax highlighter for notepad++. I myself use Emacs
on Linux. Text search/navigation and mass-text manipulation, managing
multiple buffers and sub-windows, running a Bash terminal within
Emacs, and setting it up to open *.zsc
files in c-mode
gives me
most of what I need. With a little extra work, Emacs can be turned
into a full IDE. Hell, this guide was written in Emacs (in org-mode
markup and then a modified ox-tufte.el
script with custom css
headers for export to html
, but I digress).
And get a good, wired, mechanical keyboard.
Makefile
Throughout this guide, I have had you manually executing the zip
command
to generate the pk3
archive. You technically didn’t need to generate said
archive, as GZDoom can work with whole folders. My purpose was two-fold:
(a) most people, especially when new, conceptualize a mod as a single,
share-able file. And (b) I didn’t want to bring up temporary backup files
that programs like UDB, SLADE, and text editors (even Emacs) generate when
working on files, which can duplicate code and throw errors. So a tool like
zip
was used to generate the archive with just the necessary, base files.
However, the command kept getting longer and more complicated as we added
more files and subfolders to the project. While you could create a
shell-script or a *.bat
file with the zip
command in it, and run that
every time, a better option is to use a makefile
.
Linux comes built-in with a command line tool called make
. On Windows,
there are a bunch of routes to get it working. So I never run zip
on my
doom modding projects. After I make additions/changes to the code and
assets, I run the make
command from the project-folder location. The tool
then looks for a file named makefile
at that location, and executes the
instructions therein. To illustrate with a real-world example, take a look
at the project files in one of my publicly shared mods, an IWAD template
for isometric-viewpoint games in GZDoom. Its makefile
has the following
contents:
BUILDDIR=build
ZIP=zip
ZIPFLAGS=-r -FS
ZIPEXCLUDES=-x '**~' 'build/*' '.*' 'makefile' '**.dbs' '**.backup*' '**.bak' '**.autosave*'
ZIPTARGET=$(BUILDDIR)/$(notdir $(CURDIR)).ipk3
TARGETS=$(BUILDDIR) $(ZIPTARGET)
.phony: all debug clean
all: $(TARGETS)
debug:
@echo $(BUILDDIR)
@echo $(ZIPTARGET)
@echo $(ZIPEXCLUDES)
$(BUILDDIR):
mkdir $(BUILDDIR)
$(ZIPTARGET) : *.* */*.obj */*.lmp */*.png */*.wav */*/*.png maps/*.wad *.zc */*/*.zc | $(BUILDDIR)
$(ZIP) $(ZIPFLAGS) $(ZIPTARGET) * $(ZIPEXCLUDES)
Without knowing much about makefile
syntax, you can kind-of guess what it
is doing. There are a whole bunch of variables (all capitalized) at the
start. The variable BUILDDIR
is set to the text-string “build
”. And
there is a variable named ZIPEXCLUDES
that lists a bunch of regular
expressions for files to exclude from the archive (including makefile
and
the contents of the build
subfolder). And there is a variable named
ZIPTARGET
which is using some internal make
variables like CURDIR
(stands for “current directory”). With this makefile
in my project’s root
folder, I can type make
or make all
to have the build
subfolder
automatically created if it didn’t exist, and execute the zip
command for
all of those file-types in the second-to-last line (with the excludes).
make
also keeps track of what files have changed since it last ran, so
running make
twice will not regenerate the archive (unlike a zip
command), and only files that have been changed will get added to the
archive. I can run make clean
to delete the archive in case I need a
fresh one built, and I use make debug
to make sure that I have assigned
the variables correctly (by having them printed out). I always start with a
simple makefile
in all of my projects and slowly grow its complexity with
the project over time. If you want, you can even have make
launch GZDoom
and run your archive after it builds it. Some work will be needed to hide
your local file structure from the makefile
if you end up sharing your
source code.
Git and version control
Imagine that you were maintaining a big modding project. You’ve realized
that the way you’ve set some coding structure up was inefficient. You want
to rewrite it but this would require changing a whole bunch of things that
could break the mod. So you would probably make backup copies of the files
that you want to start changing (naming them <FILENAME>.backup01.<DATE>
or something like that). Or maybe you will zip-up the whole project in its
current state into a backup archive. Half-way into your modifications, your
friend sends you a custom class in a ZScript
file that he wants you to
try. So you zip-up your current, half-finished, totally-not-working state
into another backup zip-archive, unzip the old backup, and add your
friend’s ZScript
code to start tinkering with it. A few hours later the
artist you commissioned sends you a spritesheet as promised. So you create
a third backup archive and replace your placeholder sprites in the current
project directory with the one the artist sent over. You will need to
modify the offsets in your TEXTURES
lump to make it work, so you back
that file up in case you’ll need to revert it later. And pretty soon, you
are juggling nearly full-sized copies of your project in various branched
stages of development, with no tracking information regarding what came
before what and for what reason. Now imagine if more than one person were
working on the project. Do they make independent copies of each other’s
work and email zip files over with strange names every day? What if they
work on the same file at the same time? How do you imagine that Microsoft
is developing Windows?
This is a very old problem in software development, and people have
figured out how to programmatically track these things. The technique
is called “version control”. There are many version-control software
to choose from, but the most popular one these days is called
git
. It was written by Linus Torvalds (the guy who invented the
Linux kernel). Though once it became public, lots of people have since
worked on it. The software runs on your computer and can manage your
project’s development versions over time.
You can install a GUI version of git
and learn to use that, but since I
am old-school, I use the command line on Linux systems. So I will be
showing the commands that I execute to use git
. When I start a new
project, be it a GZDoom mod, or a tutorial/guide like this website, I first
create a project folder. And inside this empty folder, I run the command:
git init
This creates a hidden subfolder named .git
(the “.
” prefix is
deliberate and not a typo). This is where git
will store all of its
content for this project. The contents of .git
should never be messed
with manually (in fact, don’t even look into that folder!). Only the
program git
is allowed to look inside and change things. After this, I
create a hidden file named .gitignore
. This file contains regular
expressions for the types of files that git
should not consider as part
of the project (i.e. to be saved). Since I use Emacs, I know that any file
I work on gets a temporary backup file created with the same name and a
tilde “~
” suffix. So the first line in my .gitignore
is always:
**~
with more lines added later over time. The double “**
” handles recursive
subfolders. After this I begin working on the project proper like normal. I
create new files and modify old ones. When I am done for the day, or I just
want to save the progress made so far before trying some crazy idea, from
the terminal I run:
git status
This will print a report of any new files that aren’t being track by git
(i.e. newly created) and are also not matching any expression in
.gitignore
. It will also print a list of files that have been modified
since the last “commit”. I use:
git add <FILENAME>
to add the new files if there are any, and then I commit the changes with the command:
git commit -a -m "<SOME MESSAGE ABOUT THIS COMMIT>"
The flag “-m
” lets me enter the message for this commit in the
command line (otherwise, git
will open a text editor and ask me to
type the commit message there). Technically, I also add the flag
“-S
” to the commit command to cryptographically sign my commits, but
that is beyond the scope of this guide. This saves the current state
of the project.
The next day, I run the command:
git checkout
to “checkout” the latest commit into the project folder before I start
working on it. At the end, I check git status
again and commit
before calling it a night. At any time, if I am not happy with the
changes that I have made but don’t know how to safely undo all the
changes that have been accumulating, I can use git checkout -f
to
checkout the latest commit again (the “-f
” means “forced, as in
allow git
to overwrite files). This will replace all the files in
the project folder with the ones from that commit. I can checkout an
earlier commit instead of the latest one too. To view older commits
(and their commit messages) I type:
git log
This shows the entire commit history with hashes and commit messages (I
type “Q
” to exit the mode). Then I can use:
git checkout <LAST-FEW-CHARACTERS-OF-THE-COMMIT-HASH>
to checkout a specific commit from the past (throw in a “-f
” to
overwrite current changes). To view all the changes (additions and
deletions) that I have made to all the text files in the project since
the last commit, I run the git diff
commmand. I can even check the
difference between any two commits using their hashes that way.
So far, we have seen a linear, chronological chain of commits. But I can
even branch my commits to try crazy experiments. And I can even merge
branches back into main (and that branching and merging history is all
stored by git
). A long-running, complex project may have several
abandoned/stale branches that had been created for various experiments and
tests over time.
All of this might seem like too much to memorize, but it becomes
second nature after a while. Most people starting out use a git
cheatsheet. Much like learning the hotkeys for UDB, you start with a
small set of 5-6 commands, practice them a bunch until you master
them, and then learn one new command at a time and incorporate it into
your workflow. Or you could just use the GUI. But it is good to get
familiar with terms like commit
, checkout
, status
, branch
,
merge
, etc. just to be aware of what is possible to do.
Remote repositories
The commit history (which is stored in a very efficient,
differences-only format by git
) is collectively called a
“repository” (or “repo” for short). The real power of version control
starts to shine when you start using remote repositories. Meaning
storing your project on a remote server that is accessible from any
computer connected to the internet. That doesn’t necessarily mean that
it is public, as you can have a private remote repo that requires a
password or a cryptographic key. Famous public git
repo servers that
offer free accounts are Bitbucket, Gitlab, and the one this website is
hosted on: Github. These are private companies (Github was acquired by
Microsoft a while ago) that don’t have anything to do with the
open-source software called git
, though their employees might be
developers. You could get a private repo going on Github if you get a
paid account.
You can view the repo hosting this website by visiting
https://github.com/dileepvr/gzdoom_modding_101
. It is a barebones repo
with very few files (the README.org
file is a symbolic link to
index.org
) and one subfolder for images. The .gitignore
file has
only one line in it. To view the commit history with the dates,
messages, and the last few characters of the hashes, click on
“commits” below the green “code” button, or simply visit this
link. This is just a linear chain with only the “master” branch, but
you can checkout this repository and view earlier versions of the
guide to see how it has evolved over time.
So to work on this website, my workflow is only slightly different from the one above. When I start, I run:
git pull
git checkout
before I begin. The git pull
command pulls all the changes that the
remote repo as accrued (very useful if you have multiple collaborators).
After I am done for the day, I run the commit
command, and then run:
git push
to push my latest commits to the remote repo. When I start working on
it from, say, one of my other computers (like the laptop while
traveling), I can git pull
the changes that I pushed from my desktop
earlier and continue working. I just need to remember to git push
after I am done! If I want to start working in it on an entirely new
computer that didn’t have the project repo to begin with, I run:
git clone git@github.com:dileepvr/gzdoom_modding_101.git
to clone the repo on my local machine, and then start cracking. You can clone any publicly available repo, by the way. You just won’t be able to push changes to it if you don’t have write-access.
The Github docs page contains instructions for starting a new repo. You can
either create it on the website and clone locally, or start git init
on
the local machine and then push the first commit to your Github account.
You may either use the website’s GUI, the git
program’s GUI, or the
command line to manage remote repos. You don’t have to push everything you
commit either. You can have local branches that don’t get pushed to the
remote repo (useful for clean, collaborative projects). When trying to
merge two branches that have diverged too much, git
can report a “merge
conflict” and will create diffs inside files, giving you the opportunity to
resolve the conflicts by making choices about which conflicting blocks of
text to keep or discard. But that is an advanced topic.
The isometric-camera IWAD template that I mentioned earlier is also
hosted on Github. This one has a “license.txt
” file in addition to a
“readme.org
” file. Its commit history is also linear with no
branches, but it has a couple of “tags” named “v1.0” and “v1.1” that
I’ve designated as “Release candidates” using Github’s facility for the
same. These were particularly stable commits that I was happy
with. There is a “Releases” tab in the right-panel on the repo’s home
page. The .gitignore
file in this repo has a few more lines to cover
the backup files created by programs like UDB and Slade.
Lastly, GZDoom itself is hosted on and actively
developed on Github. It has over a
hundred branches and tags (some of which are release versions). You can
click on the “master” pull-down button to the top left in the home page to
see some of the branch names. In the right-panel, you can see a list of the
major contributors, some of whom can push to the repo directly. They have
the keys to the engine source and make the major decisions. In the top,
there is a tab for “Issues.” This is where anyone can report bugs or make
feature requests that is visible to the public (and anyone can contribute
to the discussion). Right next to it, there is a tab called “Pull
Requests”, where contributions from third-party people like myself (who
can’t push to the repo directly) are staged before they are either accepted
or rejected. This is a Github feature (not a git
feature), and other repo
servers have similar collaborative tools. The work flow is:
- “Fork” the GZDoom repo.
- Clone the fork on my local machine.
- Start a new branch and commit some work.
- Push the branch to my forked repo.
- Create a “pull request” to the main GZDoom repo on Github’s page.
At the time of writing, there are 581 forks of GZDoom. Most of them are likely just experiments from the past that have been abandoned by various people. But engine dev is sort of irrelevant to modding. I caught the GZDoom repo home page a few hours after one of my minor pull requests (“PR” for short) had been accepted. So the latest commit briefly showed my name:

Seeking help and forum/discord etiquette
There is little point in getting into GZDoom modding if you are not also
going to get involved with the modding community. The GZDoom community is
only a subset of the greater Doom community. But you can ask for help with
general concepts (like mapping) pretty much anywhere. GZDoom-specific
queries (like anything to do with modern ZScript
) should be saved for
specific circles. For general help where the wiki falls short, you can try
the Doomworld
forums, or the subreddit r/DoomModDevs
. Naturally, these
communities have their own set of rules. Forums will enforce strict
subforum topic relevance. Subreddits will have a flair-tagging mechanism
for the same. This is meant to keep all records neatly organized and
searchable for future help-seekers. Doomworld
will very likely have a
“make five or ten replies to other threads before being allowed to start
your own” rule.
For GZDoom circles, at the time of writing, the two active places are the
ZDoom
forums and the ZDoom
Discord. The Discord server is way more live
and active, but you will find the same people (including me, come say hi!)
in both places. Discord can be such a time-sink, in fact, that Graf Zahl
(the creator and lead developer of GZDoom) has deleted his Discord account,
and only posts on the forum (and occasionally comments on Github). The
forum is where you would post stuff that you want the search-engines like
Google to crawl. So if you are posting some helpful assets or templates
(more on that in the next section), that would go in the Resources
subforum. But you can’t create a post until after you’ve made over five
replies in other threads, to prove that you are sincere. You can be banned
from the forum, so don’t be a dick! And do a thorough search for the issue
you are looking to get help on. It is possible that someone else has had
the same problem years ago and has already made a post about it. The forums
have their own Tutorials subforum.
The Discord ban hammer sees a lot more action. They get scammers, bots, and people stalking or harassing other community members by creating multiple accounts. This screenshot was from a few days ago on a GZDoom-adjacent Discord server that gets the sentiment across. Do you see the problem here:

Leaving or muting Discord servers is trivial. And a returning user would open by identifying themselves by their old account. In fact, you can peruse the thread about the Discord on the forum to see a whole bunch of people banned for various reasons, begging to be let back in.
If you are new to Discord, do not repeatedly join and leave the same
servers. That is seen as suspicious behavior. As soon as you enter the
ZDoom Discord, you will be asked to introduce yourself in the #welcome
channel. There, a moderator will give you access to all of the other
channels, once they are convinced that you are not a bot. For the first few
days, your name will have a green symbol next to it, so every post you make
will advertise your n00b status to everyone. Here is a non-exhaustive list
of channels on the GZDoom Discord:

There are more off screen, including some private ones that only special people (who have proven themselves) get access to. For example, you start doing engine development and making PRs, you might be invited to a private channel meant for discussing those. Assume that it is always turtles all the way up.
When you first join, it is generally a good idea to just lurk without making any posts. Observe the behavior to get an idea of the culture. Figure out who the jesters are, since it is impossible to guess the correct tone from text, and someone’s banter can easily be mistaken for an offensive retort if you aren’t familiar with their personality. Some people yap a lot and just want to have the last word. It is best to just let them have it, as long as you got the help that you were looking for. Restrain your ego, and leave others to their own. Figure out who the veterans are (it’s the people who are doing all the helping, answering questions, etc.). I’ve seen too many instances of someone new lecturing an actual engine dev about some historical Doom factoid. Often, if you need help with a specific mod, you can find that mod’s author hanging out on Discord. And be sure to thank the people who help you out.
Just make sure to not ping-spam anyone (that is when you use the @
to
mention their name, or reply to their posts incessantly). And try not to
send direct messages without seeking permission in one of the public
channels. Be careful about asking for commissioned help on either the forum
or Discord. You become a magnet for scammers. Don’t accept friend-requests
from strangers, as this gives them legitimacy when interacting with others
who see the friendship status (think ring of trust). If you need
commissioned help, seek out recommendations and references for commissions
from other trusted people or approach artists using the contact details on
their portfolio page (making sure that the page is old enough and the work
on it hasn’t been stolen or AI generated). All of these places have some
rules about posting AI generated assets or code.
Publishing stuff and licenses
The thing you would want to publish would be the *.pk3
file, which is
under-the-hood a zip archive. If your mod is very big (with lots of assets,
say) and you want to avoid waiting time during loading, you could find the
appropriate flags for the zip
command that will create the archive
without compressing it. Regardless, literally any file-sharing service
(like mediafire or Google Drive) can be used to generate downloadable
links. If your project is already on Github, you can tag your
current/desired commit with a version number (via a git
command) and use
Github’s “Releases” feature in the repo website’s right panel. That is a
great way to continuously update your work with patches, improvements, and
bugfixes, while maintaining version history. GZDoom itself does it like
this.
If you want to share with the GZDoom community specifically and want it to
be find-able via a Google search (or duckduckgo or what have you), creating
a thread on the ZDoom
forums is a great way to go. Here, you can receive
public feedback and commentary in the thread and update the main post as
and when newer versions become available. People on Github generally create
“Release Candidates” over there and link to it in a forum post. ModDB.com
is another popular avenue for sharing, although you can’t post links there
and need to upload the file to their server. Uploading your mod to multiple
places instead of linking makes updating versions much more difficult. If
you are taking the indie-developer persona seriously, you could create a
portfolio page on itch.io and host your mods there. For example, the
weapons and movement mod that I mentioned earlier when introducing launcher
programs (Hellrider: Vengeful) is hosted on the current project-lead’s
itch.io page. But the original mod that it is based on was announced and
updated on a forum post by its former author (Endie).
If you are using another mod-author’s work in your mod, naturally you need to have their permission; either personally, or via a license file in their mod. Speaking of which, you need to include a license file (or several) in your own mod. You also need to make it clear which parts of your mod are your work and covered by the license. Creative Commons has been suggested as a good option for artwork and assets like sound/music, but is terrible for code and map files.
A permissive choice for code, if you are stuck, is The MIT License. This is short, and covers most cases. It grants permission to others to reuse your work in theirs, even for commercial profit (and they won’t owe anything back to you). And their reuse can be published by them in any newer license of their choice.
A more restrictive option for code is the GNU General Public License (GPL). You will have to choose between GPLv2 and GPLv3 (though for most purposes, the differences won’t matter to you). This is an infectious license that allows reuse and modification but doesn’t allow closing the source (in theory). Additionally, it requires anyone else using your work to publish that work under a GPL license too. This has gotten a little tricky since GZDoom itself uses a GPLv3 license. Many dev teams have released commercial, total-conversion IWADs (standalone games) and have bundled them with some modified version of GZDoom (see Selaco and Supplice (steam link), for recent examples). If non-GPL, then licensing for such projects is in a legal grey area untested by case law. Note that this wouldn’t have been a problem if they didn’t bundle a modified GZDoom executable file with their game and just released it as an IWAD/IPK3 mod instead. Be careful, especially when using other people’s work, to check if their license restricts your licensing options like GPL does. Often, you might need to package the original author’s license file with your mod and specify what all it applies to.
If you don’t want to make things this official, just use a Beerware license. It is always worth it to include a license file (and a readme file with information on what all stuff the license covers in the project) so that people who want to use your work don’t have to track you down and ask for permission directly.