Now that weapons and entities in general are covered. This chapter is about exploring another kind of entities: monsters.
Creating a monster or NPC (shortcut for "Non-Player Character") is one of the most difficult entities to program because they have to behave in a certain way to make a game fun. Artificial Intelligence ("AI") is the component responsible for manipulating that behavior.
We will explore in this page the concepts of Half-Life's AI before we get to create a monster itself. AI and monsters programming always happen in the server project, you will likely never touch the client project. For the sake of clarity, important terminology is in
bold.
Classes
Let us do the same thing as we did for the weapons: look at the class declaration of scientist (
CScientist
), the human grunt (
CHGrunt
) and the headcrab (
CHeadcrab
) and determine the class hierarchy.
The reasons we are looking at those three monsters is that they have distinct traits: one is a "simple" monster, another one is a talkative friend and the last one has the ability to be part and communicate within a squad.
With the knowledge and experience you gained in the previous chapters of this book and/or from looking at the HL SDK by yourself, you should be able to do this on your own.
Need some help? The files you should be looking at are
scientist.cpp
,
hgrunt.cpp
and
headcrab.cpp
. Some IDEs like Visual Studio on Windows provide a "class viewer" to navigate through classes rather than files and this could help you when navigating through the code.
Have you finished? Here is the class hierarchy you should have found:
CHGrunt
└ CSquadMonster
CScientist
└ CTalkMonster
CHeadcrab
└ CBaseMonster
└ CBaseToggle
└ CBaseAnimating
└ CBaseDelay
└ CBaseEntity
If you found the correct hierarchy, congratulations! If not, do not get discouraged, learn from your mistakes and try again, that's how you gain knowledge and experience.
We can determine that those three monsters uses three different bases:
CBaseMonster
(basemonster.h
) being the very basic one that is itself the base for the others two.CSquadMonster
(squadmonster.h
) that provide squad functionality to the monster.CTalkMonster
(talkmonster.h
) for talkative monsters with dialogues such as questions, answers and so on.
The player (CBasePlayer
in player.h
) is also a "monster", its class is based on CBaseMonster
.
It may be weird but if you think about it, players and monsters share some traits like bleeding, dying, moving, attacking, be attacked and so on.
This means that a monster does not always involves an AI. For those who are familiar with Unreal Engine 4: this is a bit like having an APlayerController
or AAIController
possessing an ACharacter
(or anything derived from APawn
).
Everything has its beginning
Whenever a monster spawn through
Spawn()
, a call to
MonsterInit()
is made which is responsible of checking if the monster is allowed to spawn using the returned value of
BOOL CGameRules::FAllowMonsters()
which returns
TRUE
or
FALSE
depending on the game rules implementation used in
g_pGameRules
.
The monster remove itself immediately if that is not the case. Otherwise, it initialize some variables which some of them are described in this page.
That same method make a call to
StartMonster()
which is more interesting because it initialize the monster's "thinking" to
MonsterThink()
creating a loop of calls to
RunAI()
until the monster dies (
IsAlive()
returns
FALSE
).
RunAI()
has several responsibilities such as:
- Calling
Look()
to gather visual information like a monster has spotted an enemy, friend, nemesis. - Calling
Listen()
to gather audio information like footsteps, grenade explosions, shooting firearms. - Seeking enemies with
GetEnemy()
and determine the best one to attack based on relationship and other factors. - Check ammo if needed with
CheckAmmo()
, it is overridden and used by human grunts only.
These functions are responsible of toggling on and off
condition bits of the monster.
Condition bits
You might have seen in the code of monsters constants like
bits_COND_LIGHT_DAMAGE
. Those are
conditions bits, they are used as a way to give information to the AI like "this monster has taken light damage" for example.
They are either disabled or enabled in the monster's
m_afConditions
attribute. This is done through bitwise operation using the OR operator (
|
).
In the previous section, we said that several methods such as
Look()
and
GetEnemy()
toggle on and/or off these
conditions bits.
Look()
handle all
condition bits that involves sighting such like
bits_COND_SEE_FEAR
meaning "I see an enemy that I'm afraid of".
GetEnemy()
also does it with
bits_COND_NEW_ENEMY
for "I have a new enemy to deal with" for example. Likewise,
CheckAmmo()
does it with
bits_COND_NO_AMMO_LOADED
("my firearm is empty").
Here is a list of
condition bits common to all monsters, keep in mind that a monster may use its own (
schedule.h
):
Constant | Value | What does it mean for the monster |
---|
bits_COND_NO_AMMO_LOADED | (1 << 0) | My firearm is empty. |
bits_COND_SEE_HATE | (1 << 1) | I see an enemy that I hate (more on this later). |
bits_COND_SEE_FEAR | (1 << 2) | I see an enemy that I am afraid of (more on this later). |
bits_COND_SEE_DISLIKE | (1 << 3) | I see an enemy that I dislike (more on this later). |
bits_COND_SEE_ENEMY | (1 << 4) | I see an enemy. |
bits_COND_ENEMY_OCCLUDED | (1 << 5) | Enemy is occluded by the world. |
bits_COND_SMELL_FOOD | (1 << 6) | I smell food. |
bits_COND_ENEMY_TOOFAR | (1 << 7) | My enemy is too far. |
bits_COND_LIGHT_DAMAGE | (1 << 8) | I took light damage, just a flesh wound. |
bits_COND_HEAVY_DAMAGE | (1 << 9) | I took heavy damage, that hurts! |
bits_COND_CAN_RANGE_ATTACK1 | (1 << 10) | I can perform one kind of ranged attack. |
bits_COND_CAN_MELEE_ATTACK1 | (1 << 11) | I can perform one kind of melee attack. |
bits_COND_CAN_RANGE_ATTACK2 | (1 << 12) | I can perform another kind of ranged attack. |
bits_COND_CAN_MELEE_ATTACK2 | (1 << 13) | I can perform another kind of melee attack. |
bits_COND_PROVOKED | (1 << 15) | A friendly (likely the player) provoked me. |
bits_COND_NEW_ENEMY | (1 << 16) | I have a new enemy. |
bits_COND_HEAR_SOUND | (1 << 17) | I hear something interesting. |
bits_COND_SMELL | (1 << 18) | I smell something interesting. |
bits_COND_ENEMY_FACING_ME | (1 << 19) | My enemy is facing me. |
bits_COND_ENEMY_DEAD | (1 << 20) | My enemy is dead. |
bits_COND_SEE_CLIENT | (1 << 21) | I see a client (a bot or a player). |
bits_COND_SEE_NEMESIS | (1 << 22) | I see an enemy which I consider as my nemesis (more on this later). |
bits_COND_SPECIAL1 | (1 << 28) | First special condition specific to my type of monster. |
bits_COND_SPECIAL2 | (1 << 29) | Second special condition specific to my type of monster. |
bits_COND_TASK_FAILED | (1 << 30) | I failed to perform a task (more on this later). |
bits_COND_SCHEDULE_DONE | (1 << 31) | I completed a schedule (more on this later). |
The constant using the value (1 << 14)
is commented and not implemented which is why it is not present in the table above.
According to that comment, it was supposed to be a third kind of ranged attack (bits_COND_CAN_RANGE_ATTACK3
).
Likewise, there is no constants between (1 << 23)
and (1 << 27)
.
It is worth mentioning that
bits_COND_ALL_SPECIAL
exists and is a shortcut to include both special conditions (
(bits_COND_SPECIAL1 | bits_COND_SPECIAL2)
).
Same with
bits_COND_CAN_ATTACK
which regroup all kinds of attack (
(bits_COND_CAN_RANGE_ATTACK1 | bits_COND_CAN_MELEE_ATTACK1 | bits_COND_CAN_RANGE_ATTACK2 | bits_COND_CAN_MELEE_ATTACK2)
).
These are common methods used to handle
condition bits with an added description of what they do:
// Returns the conditions ignored by this monster (useful to ignore flinching while attacking for example)
virtual int IgnoreConditions();
// Set the condition(s) specified in "iConditions" for this monster
inline void SetConditions( int iConditions )
{
m_afConditions |= iConditions;
}
// Clear the condition(s) specified in "iConditions" for this monster
inline void ClearConditions( int iConditions )
{
m_afConditions &= ~iConditions;
}
// Returns TRUE if any condition in "iConditions" is met for this monster, FALSE otherwise
inline BOOL HasConditions( int iConditions )
{
if ( m_afConditions & iConditions )
return TRUE;
return FALSE;
}
// Returns TRUE if all conditions in "iConditions" are met for this monster, FALSE otherwise
inline BOOL HasAllConditions( int iConditions )
{
if ( (m_afConditions & iConditions) == iConditions )
return TRUE;
return FALSE;
}
Sound bits
The goal of the
Listen()
method (called by
RunAI()
) is to make the monster hear sounds he could be interested in and classify them using
sound bits such as
bits_COND_HEAR_DANGER
meaning "I heard a dangerous sound". They work in the same fashion as
condition bits.
You likely know that monsters can "smell" and react to it like Barney and scientists will comment on bad scents and bullsquids can eat headcrab carcasses.
Valve made the choice of relying the "smell" stuff on the sound stuff because they kinda work the same way hence why this is also done in Listen()
.
Here is a table of common
sound bits (and "smell bits") and what they mean (
soundent.h
):
Constant | Value | What does it mean for the monster |
---|
bits_SOUND_NONE | 0 | Nothing worth needing my attention. |
bits_SOUND_COMBAT | (1 << 0) | I hear combat, could be explosions or gunshots. |
bits_SOUND_WORLD | (1 << 1) | I hear doors opening/closing, glass breaking. |
bits_SOUND_PLAYER | (1 << 2) | I hear the player making noise (running, walking, jumping...) |
bits_SOUND_CARCASS | (1 << 3) | I smell a dead body. |
bits_SOUND_MEAT | (1 << 4) | I smell gibs or pork chop. |
bits_SOUND_DANGER | (1 << 5) | I hear danger such as a grenade bouncing, barrel about to explode... |
bits_SOUND_GARBAGE | (1 << 6) | I smell trash cans, banana peels, old fast food bags. |
Likewise, remember that monsters may use their own.
One particular method to note here is
int ISoundMask()
which returns all the bits that the monster should care about that could influence its behavior. There is also
BOOL CSound::FIsSound()
and
BOOL CSound::FIsScent()
to distinguish between sound and smell bits.
You also might have seen calls to
CSoundEnt::InsertSound( int iType, const Vector &vecOrigin, int iVolume, float flDuration )
in various places of the HL SDK that is used to insert sounds (and smells) in the world for monsters to notice.
Memory
All monsters have a
m_afMemory
attribute which act as
memory for the monster to remember various events. They also work like
condition and sound/smell bits.
Again, here is a table of common
memory bits (
monsters.h
):
Constant | Value | What does it mean for the monster |
---|
bits_MEMORY_PROVOKED | (1 << 0) | A friendly (likely the player) provoked me. |
bits_MEMORY_INCOVER | (1 << 1) | I am in a spot for cover. |
bits_MEMORY_SUSPICIOUS | (1 << 2) | A friendly (likely the player) accidently or tried to provoke me, I should be careful. |
bits_MEMORY_PATH_FINISHED | (1 << 3) | I finished moving on a specific path (used by Big Momma only). |
bits_MEMORY_ON_PATH | (1 << 4) | I'm moving on a specific path. |
bits_MEMORY_MOVE_FAILED | (1 << 5) | I already failed to move somewhere. |
bits_MEMORY_FLINCHED | (1 << 6) | I already flinched. |
bits_MEMORY_KILLED | (1 << 7) | I'm already dead (Valve recognized this as a programming hack). |
bits_MEMORY_CUSTOM4 | (1 << 8) | Fourth special memory specific to my type of monster. |
bits_MEMORY_CUSTOM3 | (1 << 9) | Third special memory specific to my type of monster. |
bits_MEMORY_CUSTOM2 | (1 << 10) | Second special memory specific to my type of monster. |
bits_MEMORY_CUSTOM1 | (1 << 11) | First special memory specific to my type of monster. |
You probably guessed already: monsters may have their own as well.
Barney is going to help us demonstrate the usage of
memory for monsters: if he is not in combat and you hurt him, he's going to remember that you provoked him (
bits_MEMORY_PROVOKED
) making him stopping following you and try to kill you.
If he is in combat and you "accidently" hurt him, he is going to get suspicious of you (
bits_MEMORY_SUSPICIOUS
) and remind you he's a friendly. If you do it again, he will get provoked (
bits_MEMORY_PROVOKED
) and you already know what is going to happen.
As usual, there are methods to manipulate all of this (with added description):
// Store the bit(s) of memory specified in "iMemory" for this monster
inline void Remember( int iMemory )
{
m_afMemory |= iMemory;
}
// Remove the bit(s) of memory specified in "iMemory" for this monster
inline void Forget( int iMemory )
{
m_afMemory &= ~iMemory;
}
// Returns TRUE if this monster remember anything in "iConditions", FALSE otherwise
inline BOOL HasMemory( int iMemory )
{
if ( m_afMemory & iMemory )
return TRUE;
return FALSE;
}
// Returns TRUE if this monster remember everything in "iConditions", FALSE otherwise
inline BOOL HasAllMemories( int iMemory )
{
if ( (m_afMemory & iMemory) == iMemory )
return TRUE;
return FALSE;
}
Capabilities
A
capability basically describe what a monster can and can't do. It is stored in the
m_afCapability
attribute and works the same as the other kinds of bits.
Here is a table of the common ones (
cbase.h
):
Constant | Value | What does it mean for the monster |
---|
bits_CAP_DUCK | (1 << 0) | I can duck. |
bits_CAP_JUMP | (1 << 1) | I can jump. |
bits_CAP_STRAFE | (1 << 2) | I can strafe. |
bits_CAP_SQUAD | (1 << 3) | I can join, form and communicate in a squad. |
bits_CAP_SWIM | (1 << 4) | I can swim. |
bits_CAP_CLIMB | (1 << 5) | I can climb ladders, ropes. |
bits_CAP_USE | (1 << 6) | I can open doors, push buttons, pull levers. |
bits_CAP_HEAR | (1 << 7) | I can hear sounds. |
bits_CAP_AUTO_DOORS | (1 << 8) | I can use automated doors. |
bits_CAP_OPEN_DOORS | (1 << 9) | I can open "manual" doors. |
bits_CAP_TURN_HEAD | (1 << 10) | I can turn my head around (if my bone controller at index 0 is correct). |
bits_CAP_RANGE_ATTACK1 | (1 << 11) | I can perform one kind of ranged attack. |
bits_CAP_RANGE_ATTACK2 | (1 << 12) | I can perform another kind of ranged attack. |
bits_CAP_MELEE_ATTACK1 | (1 << 13) | I can perform one kind of melee attack. |
bits_CAP_MELEE_ATTACK2 | (1 << 14) | I can perform another kind of melee attack. |
bits_CAP_FLY | (1 << 15) | I can fly. |
Again, a monster might have its own
capabilities. There is also
bits_CAP_DOORS_GROUP
available that is a shortcut for
bits_CAP_USE
,
bits_CAP_AUTO_DOORS
and
bits_CAP_OPEN_DOORS
together.
However, there are no methods to manipulate those. You basically set
m_afCapability
in the monster's
Spawn()
method. If for some reason you need to query if the monster is capable of something, you check directly if its bit is set (
if ( m_afCapability & bits_CAP_SOMETHING )
).
Monster state
All monster have a
monster state stored in
m_monsterState
describing in general their current state.
That state impact several things such as
tasks and
schedules selection, some generic behavior, behavior when performing scripted events (
(ai)scripted_sequence
) and so on.
Here is a table of possible states and what they mean (
util.h
):
Constant | Value | What does the monster do? |
---|
MONSTERSTATE_NONE | 0 | Nothing. |
MONSTERSTATE_IDLE | 1 | Idling. |
MONSTERSTATE_COMBAT | 2 | In combat. |
MONSTERSTATE_ALERT | 3 | Staying alert. |
MONSTERSTATE_HUNT | 4 | On the hunt (doesn't seems to be used). |
MONSTERSTATE_PRONE | 5 | Being grabbed by a barnacle or repeling down a rope (human grunt). |
MONSTERSTATE_SCRIPT | 6 | Performing a script ((ai)scripted_sequence ). |
MONSTERSTATE_PLAYDEAD | 7 | Play dead (doesn't seems to be used). |
MONSTERSTATE_DEAD | 8 | Dying/dead. |
The core of Half-Life's AI: tasks & schedules
If you go back to looking at the code of
RunAI()
, you can see there is a call to
MaintainSchedule()
. If you look at this method, you will see things about
schedules,
tasks, completion and failure.
Schedules and
tasks are the two main components of the Half-Life's AI. A
task is a set of specific actions executed by the monster like playing a sequence/animation, reload, throw a grenade and so on. A
schedule is a list of
tasks executed by the monster to achieve a bigger goal such as moving to a precise location, wave a hand, shoot and take cover to reload...
The
Task_t
structure in the code represent a
task, here is its definition:
struct Task_t
{
int iTask; // The ID of this task (TASK_WAIT for example)
float flData; // Additional data that this task may use (time to wait for TASK_WAIT for example)
};
Same thing with
Schedule_t
for a
schedule:
struct Schedule_t
{
Task_t *pTasklist; // Array of tasks for this schedule
int cTasks; // The number of tasks (always use "ARRAYSIZE" of the value you use in "pTasklist" to be safe)
int iInterruptMask; // Condition bit(s) allowed to interrupt this schedule if needed
int iSoundMask; // Sound bit(s) allowed to interrupt this schedule if needed
const char *pName; // Name of the schedule shown in the console when testing/debugging
};
Let us take a deeper look at how declared and defined a
task and a
schedule are by looking at how a monster make a small flinch:
Task_t tlSmallFlinch[] =
{
{ TASK_REMEMBER, (float)bits_MEMORY_FLINCHED }, // Remember in my memory that I flinched
{ TASK_STOP_MOVING, 0 }, // Stop moving
{ TASK_SMALL_FLINCH, 0 }, // Make a small flinch
};
Schedule_t slSmallFlinch[] =
{
{
tlSmallFlinch, // This schedule use the "tlSmallFlinch" tasks array
ARRAYSIZE( tlSmallFlinch ), // "ARRAYSIZE" will return 3 here and tell this schedule has 3 tasks
0, // No condition bit can interruot this schedule
0, // No sound bit can interrupt this schedule
"Small Flinch" // This schedule is named "Small Flinch"
},
};
How the monster is aware that it should make a small flinch? This is where
Schedule_t *GetSchedule()
intervenes:
Schedule_t *CBaseMonster::GetSchedule()
{
switch ( m_MonsterState )
{
// [...]
case MONSTERSTATE_COMBAT:
{
// [...]
else if ( HasConditions( bits_COND_LIGHT_DAMAGE ) && !HasMemory( bits_MEMORY_FLINCHED ) )
{
return GetScheduleOfType( SCHED_SMALL_FLINCH );
}
// [...]
break;
}
default:
{
ALERT( at_aiconsole, "Invalid State for GetSchedule!\n" );
break;
}
}
return &slError[0];
}
In the above example, if the monster is in combat, has the "took light damage"
condition bit enabled and does not remember in it's
memory "having already flinched", then he will try to perform a
schedule tied to
SCHED_SMALL_FLINCH
.
Now let's look at
Schedule_t *GetScheduleOfType( int Type )
to see how the
schedule is actually executed:
Schedule_t *CBaseMonster::GetScheduleOfType( int Type )
{
switch ( Type )
{
// [...]
case SCHED_SMALL_FLINCH:
{
return &slSmallFlinch[0];
}
default:
{
ALERT( at_console, "GetScheduleOfType()\nNo CASE for Schedule Type %d!\n", Type );
return &slIdleStand[0];
break;
}
}
return nullptr;
}
You can notice that
SCHED_
constants are used to identify
schedules and tie them to a
Schedule_t
.
How about
tasks? The
StartTask( Task_t *pTask )
method is called whenever a
task is requested to be performed. Let us look at
task startup for small flinch:
void CBaseMonster::StartTask( Task_t *pTask )
{
switch ( pTask->iTask )
{
// [...]
case TASK_REMEMBER:
{
Remember( (int)pTask->flData );
TaskComplete();
break;
}
// [...]
case TASK_STOP_MOVING:
{
if ( m_IdealActivity == m_movementActivity )
{
m_IdealActivity = GetStoppedActivity();
}
RouteClear();
TaskComplete();
break;
}
// [...]
case TASK_SMALL_FLINCH:
{
m_IdealActivity = GetSmallFlinchActivity();
break;
}
// [...]
default:
{
ALERT( at_aiconsole, "No StartTask entry for %d\n", (SHARED_TASKS)pTask->iTask );
break;
}
}
}
There are two interesting things to note here:
- For
TASK_REMEMBER
, pTask->flData
is passed as value to the only parameter of the Remember()
method which is bits_MEMORY_FLINCHED
as defined in the task itself. TASK_REMEMBER
and TASK_STOP_MOVING
notify they are completed successfully through TaskComplete()
because those are tasks that don't need another "thinking tick" to be completed (or failed). This is not the case for TASK_SMALL_FLINCH
.
How does
TASK_SMALL_FLINCH
notifies the AI that it has finished? This is the job of
RunTask( Task_t *pTask )
which is called by
RunAI()
. A reminder that
RunAI()
is called every "thinking tick" until the monster dies.
void CBaseMonster::RunTask( Task_t *pTask )
{
switch ( pTask->iTask )
{
// [...]
case TASK_SMALL_FLINCH:
{
if ( m_fSequenceFinished )
{
TaskComplete();
}
}
break;
// [...]
}
}
You can see here that the task will be marked as completed successfully when the sequence/animation has finished playing. We can also determine that the duration of the flinch is determined by the animation/sequence itself.
As you might have already guessed, there are scenarios where a monster cannot complete a
task successfully as intended (a monster trying to follow a target but that same target is too far and/or in a place impossible to reach). In that case, the
task is marked as failed through
TaskFail()
.
There are handy methods about
tasks available such as
int TaskIsComplete()
and
int TaskIsRunning()
. And you guessed it already, monsters can have their own
tasks and
schedules as well. You can find most of the common
tasks in the
defaultai.cpp
file.
For the
tasks that involves "cover": cover searches are made within a 764 units radius unless
float CoverRadius()
is overridden. There is also "normal cover" (
BOOL FindCover( Vector vecThreat, Vector vecViewOffset, float flMinDist, float flMaxDist )
) and "lateral cover" (
BOOL FindLateralCover( const Vector &vecThreat, const Vector &vecViewOffset )
), the difference between them is that the latter try to find cover directly to the left or right of the monster.
When defining new tasks that are specific to a monster, it is very important that the first task ID is LAST_COMMON_TASK + 1
if your monster is based on CBaseMonster
or CSquadMonster
. If you are using CTalkMonster
, use LAST_TALKMONSTER_TASK + 1
instead.
If you do not do that, you will override the common tasks that are used by all monsters.
The same applies to schedules with LAST_COMMON_SCHEDULE + 1
or LAST_TALKMONSTER_SCHEDULE + 1
.
Activities
An
activity allow tying sequences/animations in the model (MDL) with an action in the game code like idling, running, walking and so on.
If you look at any monster model with
Solokiller's Half-Life Model Viewer (decompiling the model and looking at the QC also works), you can see when browsing the sequences/animations that they may or may not be tied to a specific
activity. Idle sequences/animations are likely tied to
ACT_IDLE
, walking sequences/animations are likely tied to
ACT_WALK
and so on.
Sometimes, they are not tied to an
activity but they may be used by the game code in a different way, if you look at certain monsters code, you can see the usage of
int LookupSequence( const char *label )
instead of
int LookupActivity( int activity )
. Sequences/animations not tied to an
activity are also likely used for scripting purposes (
(ai)scripted_sequence
).
It is important to note that
activities are tied to an index through an "activity map" (
activitymap.h
). Here is a list of common
activities (
activity.h
):
Constant | Value | What does it mean for the monster |
---|
ACT_RESET | 0 | Set m_Activity to this invalid value to force a reset to m_IdealActivity . |
ACT_IDLE | 1 | Normal idling. |
ACT_GUARD | 2 | Idling in a guarding stance. |
ACT_WALK | 3 | Normal walking. |
ACT_RUN | 4 | Normal running. |
ACT_FLY | 5 | Fly (and flap if appropriate). |
ACT_SWIM | 6 | Swim. |
ACT_HOP | 7 | Do a vertical jump. |
ACT_LEAP | 8 | Do a long forward jump. |
ACT_FALL | 9 | Fall. |
ACT_LAND | 10 | Land from falling or after a jump. |
ACT_STRAFE_LEFT | 11 | Strafe to the left. |
ACT_STRAFE_RIGHT | 12 | Strafe to the right. |
ACT_ROLL_LEFT | 13 | Tuck and roll to the left. |
ACT_ROLL_RIGHT | 14 | Tuck and roll to the right. |
ACT_TURN_LEFT | 15 | Turn quickly to the left while being stationary. |
ACT_TURN_RIGHT | 16 | Turn quickly to the right while being stationary. |
ACT_CROUCH | 17 | Crouch down from a standing position. |
ACT_CROUCHIDLE | 18 | Stay crouched (loops). |
ACT_STAND | 19 | Stand up from a crouched position. |
ACT_USE | 20 | Use something. |
ACT_SIGNAL1 | 21 | First way of making a signal. |
ACT_SIGNAL2 | 22 | Second way of making a signal. |
ACT_SIGNAL3 | 23 | Third way of making a signal. |
ACT_TWITCH | 24 | Twitch. |
ACT_COWER | 25 | Cower. |
ACT_SMALL_FLINCH | 26 | Do a small flinch after a light damage. |
ACT_BIG_FLINCH | 27 | Do a big flinch after a heavy damage. |
ACT_RANGE_ATTACK1 | 28 | Do the first type of ranged attack. |
ACT_RANGE_ATTACK2 | 29 | Do the second type of ranged attack. |
ACT_MELEE_ATTACK1 | 30 | Do the first type of melee attack. |
ACT_MELEE_ATTACK2 | 31 | Do the second type of melee attack. |
ACT_RELOAD | 32 | Reload the firearm. |
ACT_ARM | 33 | Draw the firearm. |
ACT_DISARM | 34 | Holster the firearm. |
ACT_EAT | 35 | Eat some tasty food (loop). |
ACT_DIESIMPLE | 36 | Simple death. |
ACT_DIEBACKWARD | 37 | Die and fall backward. |
ACT_DIEFORWARD | 38 | Die and fall forward. |
ACT_DIEVIOLENT | 39 | Violent death. |
ACT_BARNACLE_HIT | 40 | Barnacle tongue hits a monster. |
ACT_BARNACLE_PULL | 41 | Barnacle is lifting the monster (loop). |
ACT_BARNACLE_CHOMP | 42 | Barnacle latches on to the monster. |
ACT_BARNACLE_CHEW | 43 | Barnacle is holding the monster in its mouth (loop). |
ACT_SLEEP | 44 | Take a nap. |
ACT_INSPECT_FLOOR | 45 | Look at something on or near the floor. |
ACT_INSPECT_WALL | 46 | Look at something directly ahead of you (doesn't have to be a wall or on a wall). |
ACT_IDLE_ANGRY | 47 | Pissed off idling (loop). |
ACT_WALK_HURT | 48 | Wounded walking (loop). |
ACT_RUN_HURT | 49 | Wounded running (loop). |
ACT_HOVER | 50 | Idle while in flight. |
ACT_GLIDE | 51 | Fly (don't flap). |
ACT_FLY_LEFT | 52 | Turn left in flight. |
ACT_FLY_RIGHT | 53 | Turn right in flight. |
ACT_DETECT_SCENT | 54 | Smells a scent carried by the air. |
ACT_SNIFF | 55 | Sniff something in front of the monster. |
ACT_BITE | 56 | Eat something in a single bite (the difference with ACT_EAT is that this one does not loop). |
ACT_THREAT_DISPLAY | 57 | Demonstrate without attacking that I'm angry (yelling for example). |
ACT_FEAR_DISPLAY | 58 | Just saw something scary of feared. |
ACT_EXCITED | 59 | Show excitement (like seeing some very tasty food to eat). |
ACT_SPECIAL_ATTACK1 | 60 | First special attack. |
ACT_SPECIAL_ATTACK2 | 61 | Second special attack. |
ACT_COMBAT_IDLE | 62 | Agitated idle. |
ACT_WALK_SCARED | 63 | Scared walking. |
ACT_RUN_SCARED | 64 | Scared running. |
ACT_VICTORY_DANCE | 65 | Do a victory dance (after killing a player for example). |
ACT_DIE_HEADSHOT | 66 | Die from a hit in the head. |
ACT_DIE_CHESTSHOT | 67 | Die from a hit in the chest. |
ACT_DIE_GUTSHOT | 68 | Die from a hit in the gut. |
ACT_DIE_BACKSHOT | 69 | Die from a hit in the back. |
ACT_FLINCH_HEAD | 70 | Flinch from a hit in the head. |
ACT_FLINCH_CHEST | 71 | Flinch from a hit in the chest. |
ACT_FLINCH_STOMACH | 72 | Flinch from a hit in the stomach. |
ACT_FLINCH_LEFTARM | 73 | Flinch from a hit in the left arm. |
ACT_FLINCH_RIGHTARM | 74 | Flinch from a hit in the right arm. |
ACT_FLINCH_LEFTLEG | 75 | Flinch from a hit in the left leg. |
ACT_FLINCH_RIGHTLEG | 76 | Flinch from a hit in the right leg. |
ACT_RESET
is the only activity that is not part of the activity map due to its special behavior.
Surprisingly, no monsters have their own
activities.
It is possible to have multiple sequences/animations tied to a same
activity. In that scenario, the algorithm (simplified) is:
- Forget sequences/animations not tied to the requested activity.
- Determine the highest activity weight in the potential candidates.
- Forget the sequences/animations that do not have that highest weight.
- Randomly pick one of the remaining candidates.
An use case for this is choosing a random idle sequence/animation from three possible choices whenever a monster is idling.
All monsters have three attributes named
m_Activity
,
m_IdealActivity
and
m_movementActivity
. They indicate the current
activity, ideal next one and the one to use for movement respectively. For methods, there are
SetActivity( Activity newActivity )
and
Stop()
that you might find useful.
They are common to all monsters and if for whatever reason you need to add, edit and delete one or many of them, exercise extreme caution.
You have to treat them and the activity map in the same way you would treat an "exposed C++ interface". If you add a new activity in the middle of the map for example, you will break all monsters unless you update everything including all monsters MDL files. The same applies for deleting an entry and/or changing the order in the map.
Updating the game code will not be enough, you will also be required to build and use a custom version of StudioMDL, the tool to compile QC files into MDLs. If you do not do that, StudioMDL will not be "aware" of the changes you made.
In a nutshell, making any change to activities and the activity map is "extraordinary", only do it if you really think it is really worth it and if you are aware of the consequences.
Animation events
A model can contains events allowing executing specific things such as showing a muzzle flash, play a sound and more. For monsters, it is a way to communicate with the game code and it is an important aspect to consider.
If you look at the scientist model (
models/scientist.mdl
), more specifically the
give_shot
animation, we can see some valuable information: there is a tie to the
ACT_MELEE_ATTACK1
activity and there is one
animation event tied to this sequence/animation. If we look at the event's information, it is triggered on the 17th frame and has the ID #1.
Yes, the name of the activity does not always match the real context of the sequence/animation.
The reason for this and you probably have guessed it is because of activities. Why would you risk breaking all monsters because you added an extra activity (and updated the activity map) only to match the context of a single action for only one kind of monster instead of using an existing activity that the same monster isn't already using?
So do not be surprised if you see things like ACT_EAT
being used for a sequence/animation that does not involves "eating".
HandleAnimEvent( MonsterEvent_t *pEvent )
is the method responsible for handling
animation events. If we look at the scientist's version, we can see this code:
// For the context:
// "SCIENTIST_AE_HEAL" = 1
// "SCIENTIST_AE_NEEDLEON" = 2
// "SCIENTIST_AE_NEEDLEOFF" = 3
// "NUM_SCIENTIST_HEADS" = 4
void CScientist::HandleAnimEvent( MonsterEvent_t *pEvent )
{
switch ( pEvent->event )
{
case SCIENTIST_AE_HEAL: // Heal my target (if within range)
Heal();
break;
case SCIENTIST_AE_NEEDLEON:
{
int oldBody = pev->body;
pev->body = (oldBody % NUM_SCIENTIST_HEADS) + NUM_SCIENTIST_HEADS * 1;
}
break;
case SCIENTIST_AE_NEEDLEOFF:
{
int oldBody = pev->body;
pev->body = (oldBody % NUM_SCIENTIST_HEADS) + NUM_SCIENTIST_HEADS * 0;
}
break;
default:
CTalkMonster::HandleAnimEvent( pEvent );
}
}
Taking again our event as example, it will call
HandleAnimEvent( MonsterEvent_t *pEvent )
with a value of "1" for
pEvent->event
thus calling
Heal()
to heal the player (if conditions are still met). You can also note that there are
animation events to show and hide the syringe itself as well. Other examples includes shooting firearms, shooting projectiles, throwing grenades.
Animation events may be common to several entities or specific to a particular one.
Classification & relationship
All entities even non-monsters have a
Classify()
method that returns its
classification, here is the table of them (
cbase.h
):
Constant | Value | What does it mean for the entity |
---|
CLASS_NONE | 0 | This entity does not need a classification. |
CLASS_MACHINE | 1 | This entity is a machine (sentry, turret). |
CLASS_PLAYER | 2 | This entity is a player. |
CLASS_HUMAN_PASSIVE | 3 | This entity is a passive human (scientist). |
CLASS_HUMAN_MILITARY | 4 | This entity is a human with military knowledge (Barney). |
CLASS_ALIEN_MILITARY | 5 | This entity is an alien with military knowledge (alien grunt). |
CLASS_ALIEN_PASSIVE | 6 | This entity is a passive alien (not used by any existing monster). |
CLASS_ALIEN_MONSTER | 7 | This entity is a not so friendly alien monster (Big Momma, Gargantua). |
CLASS_ALIEN_PREY | 8 | This entity is an alien prey for predators (headcrab). |
CLASS_ALIEN_PREDATOR | 9 | This entity is an alien predator on the hunt for preys (bullsquid). |
CLASS_INSECT | 10 | This entity is an insect (cockroach). |
CLASS_PLAYER_ALLY | 11 | This entity is an ally for the player (Barney & scientist). |
CLASS_PLAYER_BIOWEAPON | 12 | This entity is an alien projectile fired by a player (player hornets). |
CLASS_ALIEN_BIOWEAPON | 13 | This entity is an alien projectile fired by an alien monster (alien grunt hornets). |
CLASS_BARNACLE | 99 | Special classification for barnacles. |
Whenever a
relationship needs to be tested, a call to the method
IRelationship
is made and returns one of the following values (
monsters.h
):
Constant | Value | What does it mean for the monster |
---|
R_AL | -2 | Ally - This monster is my ally and a friend. |
R_FR | -1 | Fear - I am afraid of this monster, I need to get away from him. |
R_NO | 0 | No relationship - Not my friend but not my enemy either. |
R_DL | 1 | Dislike - This is an enemy I need to attack him. |
R_HT | 2 | Hate - I hate this enemy and I will attack him in priority. |
R_NM | 3 | Nemesis - I really hate this enemy and I want him dead no matter what. |
All of them means "enemy" but the difference is the severity or priority of the threat when choosing the enemy to attack if many of them are present.
- "Dislike" can be compared to "low priority" or "I can deal with it later if there is a more important enemy".
- "Hate" is the "medium priority", in other words, this is the entity thinking "I should probably take care of this kind of target first before dealing with the rest".
- "Nemesis" is the "highest priority", the entity will prioritize this target no matter what, anything else does not matter.
A matrix is built to determine the
relationship between two entities based on their
classification, that matrix is (
static int iEnemy[14][14]
defined in
int CBaseMonster::IRelationship( CBaseEntity *pTarget )
):
Matrix | CLASS_NONE | CLASS_MACHINE | CLASS_PLAYER | CLASS_HUMAN_PASSIVE | CLASS_HUMAN_MILITARY | CLASS_ALIEN_MILITARY | CLASS_ALIEN_PASSIVE | CLASS_ALIEN_MONSTER | CLASS_ALIEN_PREY | CLASS_ALIEN_PREDATOR | CLASS_INSECT | CLASS_PLAYER_ALLY | CLASS_PLAYER_BIOWEAPON | CLASS_ALIEN_BIOWEAPON |
---|
CLASS_NONE | R_NO (ignore) | R_NO (ignore) | R_NO (ignore) | R_NO (ignore) | R_NO (ignore) | R_NO (ignore) | R_NO (ignore) | R_NO (ignore) | R_NO (ignore) | R_NO (ignore) | R_NO (ignore) | R_NO (ignore) | R_NO (ignore) | R_NO (ignore) |
CLASS_MACHINE | R_NO (ignore) | R_NO (ignore) | R_DL (dislike) | R_DL (dislike) | R_NO (ignore) | R_DL (dislike) | R_DL (dislike) | R_DL (dislike) | R_DL (dislike) | R_DL (dislike) | R_NO (ignore) | R_DL (dislike) | R_DL (dislike) | R_DL (dislike) |
CLASS_PLAYER | R_NO (ignore) | R_DL (dislike) | R_NO (ignore) | R_NO (ignore) | R_DL (dislike) | R_DL (dislike) | R_DL (dislike) | R_DL (dislike) | R_DL (dislike) | R_DL (dislike) | R_NO (ignore) | R_NO (ignore) | R_DL (dislike) | R_DL (dislike) |
CLASS_HUMAN_PASSIVE | R_NO (ignore) | R_NO (ignore) | R_AL (ally) | R_AL (ally) | R_HT (hate) | R_FR (fear) | R_NO (ignore) | R_HT (hate) | R_DL (dislike) | R_FR (fear) | R_NO (ignore) | R_AL (ally) | R_NO (ignore) | R_NO (ignore) |
CLASS_HUMAN_MILITARY | R_NO (ignore) | R_NO (ignore) | R_HT (hate) | R_DL (dislike) | R_NO (ignore) | R_HT (hate) | R_DL (dislike) | R_DL (dislike) | R_DL (dislike) | R_DL (dislike) | R_NO (ignore) | R_HT (hate) | R_NO (ignore) | R_NO (ignore) |
CLASS_ALIEN_MILITARY | R_NO (ignore) | R_DL (dislike) | R_HT (hate) | R_DL (dislike) | R_HT (hate) | R_NO (ignore) | R_NO (ignore) | R_NO (ignore) | R_NO (ignore) | R_NO (ignore) | R_NO (ignore) | R_DL (dislike) | R_NO (ignore) | R_NO (ignore) |
CLASS_ALIEN_PASSIVE | R_NO (ignore) | R_NO (ignore) | R_NO (ignore) | R_NO (ignore) | R_NO (ignore) | R_NO (ignore) | R_NO (ignore) | R_NO (ignore) | R_NO (ignore) | R_NO (ignore) | R_NO (ignore) | R_NO (ignore) | R_NO (ignore) | R_NO (ignore) |
CLASS_ALIEN_MONSTER | R_NO (ignore) | R_DL (dislike) | R_DL (dislike) | R_DL (dislike) | R_DL (dislike) | R_NO (ignore) | R_NO (ignore) | R_NO (ignore) | R_NO (ignore) | R_NO (ignore) | R_NO (ignore) | R_DL (dislike) | R_NO (ignore) | R_NO (ignore) |
CLASS_ALIEN_PREY | R_NO (ignore) | R_NO (ignore) | R_DL (dislike) | R_DL (dislike) | R_DL (dislike) | R_NO (ignore) | R_NO (ignore) | R_NO (ignore) | R_NO (ignore) | R_FR (fear) | R_NO (ignore) | R_DL (dislike) | R_NO (ignore) | R_NO (ignore) |
CLASS_ALIEN_PREDATOR | R_NO (ignore) | R_NO (ignore) | R_DL (dislike) | R_DL (dislike) | R_DL (dislike) | R_NO (ignore) | R_NO (ignore) | R_NO (ignore) | R_HT (hate) | R_DL (dislike) | R_NO (ignore) | R_DL (dislike) | R_NO (ignore) | R_NO (ignore) |
CLASS_INSECT | R_FR (fear) | R_FR (fear) | R_FR (fear) | R_FR (fear) | R_FR (fear) | R_NO (ignore) | R_FR (fear) | R_FR (fear) | R_FR (fear) | R_FR (fear) | R_NO (ignore) | R_FR (fear) | R_NO (ignore) | R_NO (ignore) |
CLASS_PLAYER_ALLY | R_NO (ignore) | R_DL (dislike) | R_AL (ally) | R_AL (ally) | R_DL (dislike) | R_DL (dislike) | R_DL (dislike) | R_DL (dislike) | R_DL (dislike) | R_DL (dislike) | R_NO (ignore) | R_NO (ignore) | R_NO (ignore) | R_NO (ignore) |
CLASS_PLAYER_BIOWEAPON | R_NO (ignore) | R_NO (ignore) | R_DL (dislike) | R_DL (dislike) | R_DL (dislike) | R_DL (dislike) | R_DL (dislike) | R_DL (dislike) | R_DL (dislike) | R_DL (dislike) | R_NO (ignore) | R_DL (dislike) | R_NO (ignore) | R_DL (dislike) |
CLASS_ALIEN_BIOWEAPON | R_NO (ignore) | R_NO (ignore) | R_DL (dislike) | R_DL (dislike) | R_DL (dislike) | R_AL (ally) | R_NO (ignore) | R_DL (dislike) | R_DL (dislike) | R_NO (ignore) | R_NO (ignore) | R_DL (dislike) | R_DL (dislike) | R_NO (ignore) |
The way to read this matrix is simple, the correct line is the entity's
classification determining the
relationship and the column is the target's
classification.
Keep in mind that entities can override
int IRelationship( CBaseEntity *pTarget )
and thus have a specific
relationship with one or more specific entities in specific cases.
Let's take a look at the human grunt (
int CHGrunt::IRelationship( CBaseEntity *pTarget )
)
relationship code for an example:
int CHGrunt::IRelationship( CBaseEntity *pTarget )
{
if ( FClassnameIs( pTarget->pev, "monster_alien_grunt" ) || ( FClassnameIs( pTarget->pev, "monster_gargantua" ) ) )
{
return R_NM;
}
return CSquadMonster::IRelationship( pTarget );
}
We can determine that human grunts are really pissed off against alien grunts and gargantuas that they want them dead at all costs because they treat them as "nemesis".
There is no override of
int IRelationship( CBaseEntity *pTarget )
in
CSquadMonster
(the human grunt's base class) so we fall back to the matrix shown above defined in
CBaseMonster
instead.
From there, we have multiple cases depending on the target:
- They hate players, alien military and player allies like Barney (
R_HT
on CLASS_PLAYER
, CLASS_ALIEN_MILITARY
and CLASS_PLAYER_ALLY
). - They dislike passive humans like scientists, passive aliens, alien monsters like Vortigaunts, alien preys like headcrabs, alien predators like bullsquids (
R_DL
on CLASS_HUMAN_PASSIVE
, CLASS_ALIEN_PASSIVE
, CLASS_ALIEN_MONSTER
, CLASS_ALIEN_PREY
and CLASS_ALIEN_PREDATOR
). - They ignore the rest (
R_NO
on classifications not mentioned in this list).
Other attributes
There are other attributes common to all monsters that are not mentioned (yet for some of them) in this page. However, they do deserve to be known but they don't need a dedicated section to explain what they actually do.
Here is a table of some of them (
basemonster.h
):
Attribute | Description |
---|
EHANDLE m_hEnemy | Current enemy. |
EHANDLE m_hTargetEnt | Current target (to follow or to move to). |
EHANDLE m_hOldEnemy[MAX_OLD_ENEMIES] | Old enemies (MAX_OLD_ENEMIES = 4 ). |
Vector m_vecOldEnemy[MAX_OLD_ENEMIES] | Position of old enemies. |
float m_flFieldOfView | Field of view of the monster (not in degrees, 0.5f is 180 degrees to give an idea of the scale) |
float m_flWaitFinished | Game time to tell the monster that the wait is over (defined by waiting tasks). |
float m_flMoveWaitFinished | Game time to tell the monster that the wait before doing any kind of movement is over. |
int m_LastHitGroup | Last hitgroup being hit (head, chest, stomach...) |
Schedule_t *m_pSchedule | Current schedule. |
Vector m_vecEnemyLKP | Last known position of the current enemy. |
int m_cAmmoLoaded | How much ammo the firearm has. |
float m_flNextAttack | Time before making a new attack. |
int m_bloodColor | Color of the blood when being hurt (red, yellow...) |
int m_failSchedule | Schedule to use in case of a task failure. |
float m_flHungryTime | Time before eating something again. |
float m_flDistTooFar | Range in units to determine that the monster is too far from its enemy (see condition bit bits_COND_ENEMY_TOO_FAR ). |
float m_flDistLook | Range in units to acquire enemies. |
int m_iTriggerCondition | For (ai)scripted_sequence , this is the condition to trigger something (half health, see a player...) |
string_t m_iszTriggerTarget | For (ai)scripted_sequence , this is the name of the entity to trigger. |
Vector m_HackedGunPos | The position of the firearm relative to the monster's origin. |
Recap
This page has a lot of information to digest so you might want a recap of everything we have seen so far:
- There are multiple base classes for monsters providing different functionality but they all share a common base.
- A monster does not always involve an AI (player).
- Condition bits are used by monsters