Monsters Programming - Sounds, smells and the system behind them Last edited 3 months ago2024-01-22 15:37:15 UTC

Half-Life Programming

In the first and previous pages of this chapter, we talked about the ability for monsters to listen and smell. However, we haven't talked in details about how the sounds and scents themselves are handled so this page is here to fix this void.

We're going to start by answering the following question: "how a sound and smell is represented in the code?". The answer to said question is the CSound class:

Sounds

class CSound
{
public:
    void Clear();
    void Reset();

    Vector m_vecOrigin;   // sound's location in space
    int m_iType;          // what type of sound this is
    int m_iVolume;        // how loud the sound is
    float m_flExpireTime; // when the sound should be purged from the list
    int m_iNext;          // index of next sound in this list ( Active or Free )
    int m_iNextAudible;   // temporary link that monsters use to build a list of audible sounds

    bool FIsSound();
    bool FIsScent();
};
Some information in this class is explicit like the origin of the sound, type and volume. You'll notice the comments refer to something called a "list" involving "active" and "free" things, we will talk about them later. If you read and remember what has been said in the first page correctly, you should be able to answer the "why scent is a sound" question by yourself. If not, it is strongly advised you (re)start reading this chapter from the beginning.

The CSound::Clear() and CSound::Reset() methods both reset the sound/scent values to their default which is "no origin, no volume, no type" and so on, the difference is that is that Clear() does expire/unlink the sound/scent from the mentioned "list" while Reset() doesn't thus keeping said sound/scent in said "list".

m_iType possible values are:
#define bits_SOUND_NONE    0
#define bits_SOUND_COMBAT  (1 << 0) // gunshots, explosions
#define bits_SOUND_WORLD   (1 << 1) // door opening/closing, glass breaking
#define bits_SOUND_PLAYER  (1 << 2) // all noises generated by player. walking, shooting, falling, splashing
#define bits_SOUND_CARCASS (1 << 3) // dead body
#define bits_SOUND_MEAT    (1 << 4) // gib or pork chop
#define bits_SOUND_DANGER  (1 << 5) // pending danger. Grenade that is about to explode, explosive barrel that is damaged, falling crate
#define bits_SOUND_GARBAGE (1 << 6) // trash cans, banana peels, old fast food bags.
Through these values, you'll notice the usage of bitwise operations and you can guess that bool CSound::FIsSound() would return true for combat, world, player and danger. bool CSound::FIsScent() would return true for the other values being carcass, meat and garbage.

The system

Now that we know how a sound/smell is representated, it's time we talk about that so-called "list". When the world (worldspawn entity) is being precached, it creates a soundent entity. That entity is tied to a class you used previously multiple times and might already be familiar to you: CSoundEnt.

We're not going to do the same thing as we did for CSound which is look at the class definition of CSoundEnt. We're going to "follow the code" instead, just know that the entity is marked as "do not save" (int CBaseEntity::ObjectCaps() is overriden to return FCAP_DONT_SAVE) so the contents/state won't be persisted through maps transitions and saved games.

When spawning, it "initialize" its m_SoundPool member which is an array of CSound with a capacity of 64 as defined by the MAX_WORLD_SOUNDS constant. In other words, there is a loop which call CSound::Clear() and set m_iNext to i + 1. The last sound/scent's m_iNext is set to SOUNDLIST_EMPTY which is equals to "-1". Several methods that will be described below will perform a "is there room for another sound/scent" check to prevent dangerous overflows.

There is another loop which "reserve" X amounts of sounds/smells where X is the maximum amount of clients (players and bots) on the server (gpGlobals->maxClients). That "reservation" calls int CSoundEnt::IAllocSound() and set the sound/smell expiration time to SOUND_NEVER_EXPIRE which is also -1.
Important difference about client iteration in loops
By "default", when you need to iterate on all clients in "gameplay code", one of the many possible loops to achieve this looks something like this:
for (int i = 1; i <= gpGlobals->maxClients; i++)
{
    CBasePlayer* pPlayer = UTIL_PlayerByIndex(i);
    if (!pPlayer)
        continue;

    // Do stuff here
}
Notice that the loop iterator (i) starts on 1 rather than 0 as well as the usage of "less or equals than" instead of "less than" in the ending condition. That's because entity index #0 is always the world (worldspawn), index #1 is always the first (and only if singleplayer) client and the next indexes are either other clients (if multiplayer) or entities depending on the value of gpGlobals->maxClients.

In the sound/scent system, the world cannot produce such things and thus index #0 is always the first/only client and index #1 would be the second client (if multiplayer) or map entities (if singleplayer), again, depending on gpGlobals->maxClients's value.

This causes an annoying "offset" if we were to use the same kind of loop with the same starting value and operator in the ending condition. Luckily for us, Valve provided the convenient int CSoundEnt::ClientSoundIndex(edict_t* pClient) method that gives you the right index of the sound/smell in the pool produced by pClient which is the entity dictionary of the concerned client.
Let's dive deeper into that int CSoundEnt::IAllocSound() method, it "allocate" a sound by moving two indexes in the pool (m_SoundPool) which are m_iActiveSound and m_iFreeSound. We can determine that there is a "linked list" approach being used where m_iActiveSound act as the index to the "head/first" sound/smell and m_iFreeSound being the one for the "last" element. Those sounds/scents are linked together through m_iNext.

Once that "reservation" is done, it disable or enable a "verbose report functionality" depending on the value of the displaysoundlist CVAR.

Since it's an entity, there is a one second delay to "think" being applied before ending the whole "initialization" process. It's time to look at what happens during the thinking process.

Once that second delay has passed, the thinking now happens every 0.3 seconds indefinitely (unless the entity is killed or the game is paused of course). It loops over the pool of sounds/scents and check if their expiration time has passed or not. Assuming it has, it is freed (CSoundEnt::FreeSound(int iSound, int iPrevious)) while taking care that new links are made. Remember, we're dealing with a "linked list" so deleting an element implies breaking its link with the previous and next element and the two latter needs to be linked (unless specific scenario like there is only one element).

By the way, you can query the indexes of those "active" and "free" sounds/scents with int CSoundEnt::ActiveList() and int CSoundEnt::FreeList(), the method that handles listening (CBaseMonster::Listen()) for monsters uses the first one to skip iterating over the whole pool and avoid unnecessary iterations (optimization).

If the report is enabled, then the amount of "active" and "free" sounds/scents is calculated through two calls to int CSoundEnt::ISoundsInList(int iListType), one with SOUNDLIST_ACTIVE and the other with SOUNDLIST_FREE. Speaking of amount, it is possible to check if the list of sounds/scents is empty through the bool CSoundEnt::IsEmpty() method.

And that concludes the "thinking" part, there is one method we need to talk about and it's the most important one. You've been using it for so many time with weapons and monsters emitting non-vocal sounds, it's of course: CSoundEnt::InsertSound(int iType, const Vector& vecOrigin, int iVolume, float flDuration).

Explaining this method's inner-working is going to be very brief, it "allocates" a sound/scent and set the requested origin, type, volume and expiration time relative to the game time (gpGlobals->time).

Another handy method you need to be aware of when working on monsters themselves is: CSound* CSoundEnt::SoundPointerForIndex(int iIndex) which basically give you the sound/scent itself at the desired index.

Interactions with players

While this section does not concern monsters per-se, players produces sounds (and scents indirectly through gibs when they die) that could make their AI react, and this is the part we're going to look at.

Everything happens in CBasePlayer::UpdatePlayerSound(), there is a body volume that start at 0 if the player is in the air or the velocity's length with a maximum cap of 512. Jumping add an extra 100 to the body volume (no maximum cap on this one).

If the weapon's volume (m_iWeaponVolume) is higher than the body volume, then that volume is used and the "combat" flag is set on the sound. Otherwise, the body volume is used instead. This is tracked with m_iTargetVolume and this is also where the weapon volume silence over time. If you have no idea what "weapon volume" is, maybe you should consider read (again) the "Weapons Programming" chapter.

Finally, if the sound's volume is higher than the one chosen before, the final value is determined straight away. If it's the opposite, it silences itself over time. If the "silence" cheat (impulse 105) is enabled, the final volume is overriden to 0. Regardless of that cheat's state, the sound's origin is set to the same as the player's and the bits_SOUND_PLAYER type is added.

There is also an "extra sound type" feature (int m_iExtraSoundTypes and float m_flStopExtraSoundTime in CBasePlayer) for the player, this is used by the 9mmAR's grenade launcher to insert a "combat" sound for the AI to hear. Unrelated to sounds/scents, this is where the weapon's muzzleflash (m_iWeaponFlash) decays as well.

Interactions with monsters

Monsters have a concept of "best sound" (CSound* CBaseMonster::PBestSound()) and "best scent" (CSound* CBaseMonster::PBestScent()), both uses the same code to iterate through the sound pool and retrieve the closest sound/scent.

All monsters with "military knowledge" queries the "best sound" and check if the type has the "danger" bit so they can (try to) evade grenades. Houndeyes uses it as well for their "wake up" behavior when they're in a pack, scientists uses it too when they fear danger and obviously tentacles since they can't see and only hear.

Bullsquids uses "best scent" for their "eating" behavior.

Cockroaches queries the sounds/scents pool and will go to the nearest "food" they smell in order to eat it, assuming they're hungry and not in a lit area and scared by flashlights, muzzleflashes or any other light source.

All monsters face the "best sound" when they hear one and switch to the "alert" state if it's a combat/dangerous one. They also become "alerted" if they smell anything.

Remember that when a monster hear something, the bits_COND_HEAR_SOUND condition bit is set. If it smells something, the bits_COND_SMELL is set. An additional bits_COND_SMELL_FOOD is set for "meat" and "carcass" smell types. Talkative monsters have a CTalkMonster::TrySmellTalk() method where they can comment every minute (timer and "said bits") on smells by doing an if (HasConditions(bits_COND_SMELL)) check.

Comments

You must log in to post a comment. You can login or register a new account.