Hosting and domain costs until October 2024 have been generously sponsored by dumptruck_ds. Thank you!

Creating A Monster

From Quake Wiki

Creating a new monster may seem like a daunting task at first, but this tutorial will cover all of the basics and go in-depth on every step of doing so. This will not focus on modifying an existing monster, but instead on creating a completely new one. Only QuakeC will be covered and not the limitations of modeling, texturing, audio, etc. for a monster.

This tutorial will assume the latest version of FTEQCC will be used to compile. This is highly recommended since it offers many new features to QuakeC while still remaining vanilla compatible. Syntax used for code will follow a more C-like standard as a result instead of the default QuakeC.

What Is An Entity?[edit]

The first thing to cover is what exactly an entity is. An entity is what its name sounds like: any kind of object that actually exists in the map. Everything in Quake is an entity, including the map itself (stored inside of the world entity). As such, every entity has access to every field as defined in the fields section. Some entities have no need for all of these, but they have access to them regardless.

So how does Quake know what entities are different? The answer is that it's largely cosmetic. While every entity under the hood is the same, what makes them look different is their models, what sounds they play, and what logic they run within QuakeC. All these outward details come together to form what will be a unique entity. Still, though, the game knows that an Ogre is an Ogre on map load, so what logic handles that if, internally, they're all the exact same?

Initializer Functions[edit]

Initializer functions can be seen as a type of constructor. When an entity is placed in the map editor, its classname field gets set. For instance, Ogres get set to monster_ogre. Quake's mapping format works in plain text, so when the engine is parsing through the file, it sees the entity has the key classname with a value of monster_ogre. This is where the magic happens as this value determines what function the engine calls when the entity is first placed.

If you look inside the QuakeC definition for the Ogre, you'll see a function like this:

void() monster_ogre =
{
    // Ogre stuff
};

You might notice this function has the same name as the provided classname. This is how the map knows that an Ogre is an Ogre. Within that function you'll find all kinds of fields being set such as health, model, actions like melee and running, etc. This is the logic part that causes the entity to chain its functionality into what will appear to the player as an Ogre.

In order to make a new monster, then, we must provide an initializer function that the map can make use of. Since all monsters have the monster_ prefix, we'll stick with this to keep the naming convention standard and easier for the map editor to interface with. Let's make a giant toad monster:

void monster_toad()
{
    // This is where we'll define what makes the toad a toad
}

Now any entity placed in the map editor with the classname monster_toad will call this function.

Precaching[edit]

One extremely important step to making anything is precaching any models and sounds it's going to make use of. When setmodel or sound are called, the path you pass is actually a reference to an index within the internal table the engine uses. This is done because looking these values up while the game is running would be too expensive, so all the heavy work is done at map load. As such, precaching will only work at map load; you cannot precache anything after the map has started. This means every single model and sound the monster could potentially use all need to be cached, even if they might not make use of them at all times.

This is the first thing we'll do. Since other monsters use these sound and model formats, we'll stick to the same naming conventions to keep it standardized:

void monster_toad()
{
    precache_model("progs/toad.mdl");   // Base model
    precache_model("progs/h_toad.mdl"); // Head gib model

    precache_sound("toad/idle.wav");   // Sound when moving around
    precache_sound("toad/wake.wav");   // Sound when waking up
    precache_sound("toad/attack.wav"); // Attacking sound
    precache_sound("toad/pain.wav");   // Sound when taking damage
    precache_sound("toad/death.wav");  // Sound when dying
}

We'll keep the monster simple and assume it only has one attack. When a monster gibs, they don't actually disappear but instead swap their model to their head gib model, hence why we have to precache that as well. An important note is that loading a save file works similar to loading a new map. All the entities are read in from the file and the key-value pair fields get set after their initializer function is called. If you're doing something like randomizing an entity's model, you should precache all possible options to ensure that the entity will always have access to the correct model after loading a save file.

Integrating With Game Modes[edit]

The engine will handle any difficulty settings set by the map editor for entities as those are spawn flags every entity has. However, game modes that aren't deathmatch need to be handled manually, meaning co-op. The nature of the initializer function means we can handle these however we want, but we'll be using the spawnflags field to give mappers a way to tweak it.

Since spawn flags can be customized, we can give mappers access to co-op only monsters. Spawn flags are stored as bits, so each number will correspond to a power of 2. To make this more readable, we'll create a constant:

const float SPAWNFLAG_NOT_SINGLEPLAYER = 1<<12;

This sets it to a value of 4096, or 212. If you're wondering why we use such a large value, it's because the previous four powers of 2 (1<<8, 1<<9, 1<<10, 1<<11) are internal spawn flags that determine what difficulties to spawn an entity on and if they should spawn in deathmatch. We use not logic here because this is how the engine handles it internally, and we'll be keeping it consistent. An important thing to note here is that Quake only has access to 23 bits since the flags are stored as a float, so 1<<22 is our maximum value we can set. After this the flag will start to give undesired results.

Now that we have our spawn flag, let's set up our monster to use it at the very start of the function:

void monster_toad()
{
    if (!coop && (self.spawnflags & SPAWNFLAG_NOT_SINGLEPLAYER))
    {
        remove(self);
        return;
    }

    // ...
}

This is one way to do it, but we might want other monsters to make use of it. In this case, turning it into its own function would be better since it'll prevent having to copy and paste a bunch of code, something that can lead to nasty bugs that are hard to fix. We'll give this function a more generic name just in case we want to add more conditions to it later:

float ShouldSpawn()
{
    if (!coop && (self.spawnflags & SPAWNFLAG_NOT_SINGLEPLAYER))
    {
        remove(self);
        return FALSE;
    }

    return TRUE;
}

void monster_toad()
{
    if (!ShouldSpawn())
        return;

    // ...
}

You could also use similar logic to add the opposite effect: singleplayer only monsters. That'll be left as an exercise for the reader to implement.

Describing Our Monster[edit]

Now that we have our initializer function ready to go, it's time to actually describe how the monster interacts with the world. The first thing to touch on will be its states followed by how to get the monster to use those states.

State Machine[edit]

Monsters run off what is essentially a series of logic states. They can chain into each other and loop, or be interrupted at any time to go to a new state. These states often control the animation of the entity as well since Quake has no concept of model animations. This is often confused as the animation state controlling the game logic, but in reality it works the other way around: the game logic has absolute control over every frame of the animation. It's best to think of "animation states" as a set of sequential gameplay logic that happens to set the model's frame.

There are a couple of great macros here that QuakeC gives us access to. Macros can be used to both give values an alias (similar to constants) or be used as a shortcut for boiler plate code, code that is necessary but adds bloat when constantly having to type it. The first macro that QuakeC gives us is frame macros. Since animation is handled manually, we have to assign the entity's frame manually, which is just a number. This isn't very descriptive and making constants for every single frame would be excessive:

const float TOAD_STAND1 = 0;
const float TOAD_STAND2 = 1;
const float TOAD_STAND3 = 2;
// You get the idea

QuakeC gives us a simple alternative to this: the $frame command. This allows us to give an alias to numbers without having to define them as global constants. Only the file it's defined in will have access to them. When $frame is used, it starts at a value of 0 and will continue adding one for each new value. The above constants could then be written as:

$frame stand1 stand2 stand3 // etc.

Much more convenient. We can then access it by using $ followed by the alias e.g. $stand1. An important note is that for every new line, a $frame needs to be added at the start if you wish to keep defining more.

The next macro is a simple one for defining a logic state. Logic states are actually just regular functions that can be called from anywhere at any time, but often they assume self is the entity it's applying to. Monsters use a pretty standard logic rate of 10Hz, or one logic state lasting 0.1 seconds. The three things defined by each logic state are the monster's animation frame, the next time to think (aka when to go to the next logic state), and what state it goes to. Normally this would result in extreme bloat:

void toad_stand1()
{
    self.frame = $stand1;
    self.think = toad_stand2;
    self.nextthink = time + 0.1;

    // ...
}

This would need to be done for every single logic state. With the macro, we can cut this down significantly:

void() toad_stand1 = [ $stand1, toad_stand2 ] { // ... };

This code is identical to the code above it. Since most logic states will only have a single function call in them if even that, this allows them to be very quickly written with reduced potential for bugs.

These states can technically run at any rate you'd like simply by changing nextthink:

void() toad_stand1 = [ $stand1, toad_stand2 ] =
{
  // ...
  self.nextthink = time + 0.05; // Update at 20Hz instead of 10Hz
};

This will work perfectly fine but the update rate should be kept low, not just for performance reasons but because once the game drops below the target update rate, things will begin to slow down. With an update rate of 20Hz, for instance, this monster will appear to move at half speed when the game is at 10 FPS. This isn't realistically an issue on modern machines, but is worth considering.

With all this info, we can create an animation sequence for our monster. Let's keep the loop simple and only use four states:

$frame stand1 stand2 stand3 stand4

void() toad_stand1 = [ $stand1, toad_stand2 ] = { ai_stand(); };
void() toad_stand2 = [ $stand2, toad_stand3 ] = { ai_stand(); };
void() toad_stand3 = [ $stand3, toad_stand4 ] = { ai_stand(); };
void() toad_stand4 = [ $stand4, toad_stand1 ] =
{
    if (random() < 0.2)
        sound(self, CHAN_VOICE, "toad/idle.wav", 1, ATTN_IDLE);

     ai_stand(); 
};

ai_stand() is the basic monster function that handles looking for an enemy. Given the sequence we have here, this will attempt to look for an enemy every 0.1 seconds. At the end we loop back to toad_stand1() with a 20% chance to play an idle sound. This will continue until something causes the monster to leave this state e.g. spotting an enemy.

By default monsters have access to a series of function pointer fields that functions like ai_stand() make use of. This includes th_stand, th_walk, th_run, th_die, th_melee, th_missile, and th_pain. Check out the fields page for what these do. If th_melee or th_missile aren't set, monsters will avoid trying to do these types of attacks.

The last bit is to set it from our initializer function. This part is easy.

void monster_toad()
{
    // Spawn check
    // Precaching

    self.th_stand = toad_stand1;
}

After you set up all your appropriate states you can set the rest of the function pointers. Remember, since this accepts any function, it doesn't have to actually be one of your logic states. You could instead assign a function that determines what kind of attack to use:

void StartToadMelee()
{
    if (random() < 0.5)
        toad_meleea1(); // Standard melee attack
    else
        toad_meleeb1(); // Alternative melee attack
}

void monster_toad()
{
     // ...
    self.th_melee = StartToadMelee;
}

Monster Properties[edit]

One of the last things we need to do is give an actual way for players to interact with our monster. There are a few important things here. First is the physics of the monster. This is almost always these values:

self.solid = SOLID_SLIDEBOX;
self.movetype = MOVETYPE_STEP;

This denotes what kind of collision and movement the monster uses. See the solid and movetype pages for more info. It's good to always set these before setting either the model, size, or position so when the entity is linked into the world it has the correct physics state.

Next up is the model and size. The model should always be set first and then the size after. This is because for standard entities calling setmodel() will fill in a default size that we then need to override with setsize(). Setting the model also has to come after precaching, never before:

setmodel(self, "progs/toad.mdl");
setsize(self, VEC_HULL_MIN, VEC_HULL_MAX);

You might have noticed the use of a constant here for the size. Quake's collision detection normally uses bounding boxes, but for level geometry it needs to be much more precise. This can be expensive to do so Quake instead pre-calculates collision volumes for two sizes: VEC_HULL_MIN/MAX and VEC_HULL2_MIN/MAX. You can set the sizes to whatever values you'd like, but be warned that collisions with level geometry can become inconsistent. See setsize for more information.

The last are just miscellaneous values like health and yaw_speed. These can be set to whatever you'd like:

self.health = 150; // A bit tanky for a toad

At the very end we call one of the standardized monster starting functions. These are functions that just handle setting up common monster functionality like setting the FL_MONSTER flag. Let's assume our toad will only walk. In that case we should use:

walkmonster_start();

Bringing It All Together[edit]

This is what our current monster will look like:

$frame stand1 stand2 stand3 stand4

void() toad_stand1 = [ $stand1, toad_stand2 ] = { ai_stand(); };
void() toad_stand2 = [ $stand2, toad_stand3 ] = { ai_stand(); };
void() toad_stand3 = [ $stand3, toad_stand4 ] = { ai_stand(); };
void() toad_stand4 = [ $stand4, toad_stand1 ] =
{
    if (random() < 0.2)
        sound(self, CHAN_VOICE, "toad/idle.wav", 1, ATTN_IDLE);

     ai_stand(); 
};

// These should probably be put somewhere more general
const float SPAWNFLAG_NOT_SINGLEPLAYER = 1<<12;

float ShouldSpawn()
{
    if (!coop && (self.spawnflags & SPAWNFLAG_NOT_SINGLEPLAYER))
    {
        remove(self);
        return FALSE;
    }

    return TRUE;
}

void monster_toad()
{
    if (!ShouldSpawn())
        return;

    precache_model("progs/toad.mdl");   // Base model
    precache_model("progs/h_toad.mdl"); // Head gib model

    precache_sound("toad/idle.wav");   // Sound when moving around
    precache_sound("toad/wake.wav");   // Sound when waking up
    precache_sound("toad/attack.wav"); // Attacking sound
    precache_sound("toad/pain.wav");   // Sound when taking damage
    precache_sound("toad/death.wav");  // Sound when dying

    self.th_stand = toad_stand1;

    self.solid = SOLID_SLIDEBOX;
    self.movetype = MOVETYPE_STEP;

    setmodel(self, "progs/toad.mdl");
    setsize(self, VEC_HULL_MIN, VEC_HULL_MAX);

    self.health = 150;

    walkmonster_start();
}

With this guide combined with referencing the rest of the monsters id Software created, you should now hopefully know where to go from here to create a full monster in QuakeC.