If you remember what has been said in the first page
method which is kinda like a "core loop" had several responsibilities involving senses, enemy acquisition and other things.
In this page, we will take a deeper look into said method with those responsibilities as well as some other things not mentioned in the linked page.
If we look at the
method, the first thing we can see is that idle sounds (calling
) are performed on a 1% chance (
RANDOM_LONG( 0, 99 ) == 0
) if the monster is not gagged (
spawnflag set by level designers) and is idling (
m_MonsterState == MONSTERSTATE_IDLE
) or in an alert state (
m_MonsterState == MONSTERSTATE_ALERT
After idle sounds,
Look( m_flDistLook )
is a member variable responsible to define how far the monster can look, it has a default value of 2048 units which is set in the
method. If you want to override this value, it's best to set it after the call to said method when the monster is spawning (unless the monster does not call
at all like the leech for example).
When the method is called, the monster clear any condition flag from the last frame involving enemy and sight (
and so on). If the monster has the "prisoner" spawnflag (
), then the "looking" part is considered to be done and we move on to listening (and smelling) right away.
The next step consist of retrieving all clients and monsters (entities with the
flag) in a box around the monster the size of
CBaseEntity **UTIL_EntitiesInBox( CBaseEntity **pList, int listMax, const Vector &mins, const Vector &maxs, int flagMask )
. Likely for performance reasons, the limit of "collected" entities in the box is 100.
For each client/monster in said box, in order to be taken into account it must:
- Not be the same monster performing the look (
pSightEnt != this).
- Does not have the "prisoner" spawnflag (usually for monsters).
- Be alive (health above 0).
- Has a relationship (test must not return
- In the monster's viewcone (
bool FInViewCone( pEntity ) returns true).
- Does not have the "no target" flag (
- Be visible (
bool FVisible( pEntity ) returns true).
If any of these checks above is false, then we check the next client/monster in the list.
Otherwise, there is a check if the sighted entity is the player (
returns true) and in the monster's view cone. This is to handle the "wait until seen" spawnflag (
) that level designers can set on monsters. If the monster did not saw the player that frame, we move to the next client/monster in the list. Otherwise, the spawnflag bit is cleared and the
is set (mostly used by scripting entities like scripted_sequence
A link between the monster and the sighted entity is stored in their
member variable (linked list approach), this will be used in enemy acquisition later.
If the "wait until seen" spawnflag was never set or was handled (the monster saw the player), then we check if the sighted entity is the current enemy (
). If the answer is yes, the
condition flag is set.
Finally, an additional condition flag is set based on the relationship with the sighted entity (except allies). For example: a scientist will set the
condition flag when seeing human grunts.
Listen (and smell)
Looking at the
method, we can see that listening (and by extension smelling) is done by resetting the condition flags about it (
A local variable named
retrieve what the monster can hear and smell about (the result of calling
). If the monster has a schedule (
m_pSchedule != nullptr
), then the sound mask(s) of said schedule are added.
You'll notice that conditions are cleared again and monsters have a "hearing sensitivity". Only the tentacle has a sensitivity of 2 while the others have a sensitivity of 1.
An iteration upon sounds and smells is done, for each sound that the monster care and is within it's hearing range (sound's range in units multiplied by hearing sensitivity), we check if it's a sound or smell and set the right conditions flags. Also, the type of sound (combat, danger...) is stored in
so other parts of the code can use it.
The first part is done in
. First, it check if any condition flag involving seeing a nemesis, hated or disliked monster is set.
If any of these condition flag is set, then the best visible enemy (
) is determined.
If the monster is running a schedule, had an actual enemy and the best visible enemy is not the actual one (
pNewEnemy != m_hEnemy && pNewEnemy != nullptr
), then two things could happen:
- If the schedule has a
bits_COND_NEW_ENEMY interruption mask, then the best visible enemy becomes a priority (a call to
PushEnemy( m_hEnemy, m_vecEnemyLKP ) is made, the
bits_COND_NEW_ENEMY condition flag is set and the actual enemy becomes the new one).
- If the best visible enemy has an owner which is a monster and has a "hostile" relationship, then the owner also becomes a priority (just a call to
PushEnemy( m_hEnemy, m_vecEnemyLKP )).
A reminder that monsters have an actual enemy (
) and a list of "old enemies" up to
(4) that works like a "stack" (hence why the
PushEnemy( CBaseEntity *pEnemy, Vector &vecLastKnownPos )
If the monster had no actual enemy but picked up an old one while performing a schedule with the "new enemy" interruptible mask, then the "new enemy" condition flag is set.
The method returns true or false depending if the monster has an actual enemy or not. In order for the second part
to happen, the result of the first part must be true.
Assuming the latter, the method will set or clear the condition flag that the "enemy has been occluded" (
) based on it's visibility.
Another check is made to see if the enemy is alive or dead (
). Assuming the enemy is still alive, information about the enemy's position and distance are gathered.
If the enemy is visible, a test is made to set/clear the
Otherwise, if the enemy can't be seen and occluded but the distance is less than 256 units, then the monster will magically know where the enemy is. Mods that wish to implement "stealth gameplay" should probably remove or edit this behavior (blackjack/backstabbing anyone?)
Likewise, another check if the distance between the monster and it's enemy is made, if it's higher or equals than
, then the
condition flag is set. Otherwise, it's cleared.
If the monster is allowed to check attacks (a call of
which by default check the condition flags "can see the enemy" and "enemy not too far"), then it will check the attacks (call to
). Checking attacks basically involves checking if the monster if capable of doing a kind of attack (primary/secondary melee/ranged), call the appropriate check method which is usually overriden by monsters and if the result is true, the appropriate condition flag is set. Example: if
bool CheckRangeAttack1( float flDot, float flDist )
returns true then the
condition flag is set (assuming
m_afCapability & bits_CAP_RANGE_ATTACK1
Finally, if the monster is instructed to move towards the enemy (
m_movementGoal == MOVEGOAL_ENEMY
), has a route of type "goal" or "enemy" (
bits_MF_IS_GOAL | bits_MF_TO_ENEMY
) but the enemy's last known position's (
) distance is higher than 80 units to said route, then the entire route is refreshed (
). Don't worry if you don't understand this part right away, navigation will be covered in another page when we'll get there.
If the sense and enemy acquisition condition mentioned in the introduction is true, then a call to
is made. Only the human grunts (and our Armored Man from the basic/squad monster tutorial
) override it so they can check if
is equals or less than 0 and set the condition flag that they need to reload if that's the case. You should know what happens next by now.
From here, the mentioned sense and enemy acquisition condition no longer matter.
A call to
is made, this method is very interesting for level designers because it is responsible for handling the "trigger condition" and "trigger target" properties they set on the monsters. If you don't know what "trigger conditions" are, those are basically "when the monster is dead", "when the monster has half of his health", "when the monster sees the player" and "trigger target" is the name of the entity to call/fire when said "trigger condition" is true.
Next, a call to
is made, this is an optional spot to do things before the entire schedules/tasks management happen.
After that, you should be familiar with what
does. If not, you should consider (re-)reading the page about schedules and tasks