Roaming Monsters - FreeSlave/halflife-updated GitHub Wiki
Introduction
Sometimes a mod author wants a monster to roam in the area, simulating some action, instead of just standing at the spot. Sven Co-op introduced a concept of roaming monsters. Monsters with freeroam
parameter set to Always
will freely roam between graph nodes on the map. There's also a global parameter for worldspawn
to make all monsters freeroam by default.
In this tutorial we'll implement this feature in Half-Life SDK.
Implementation
All changes that we're going to apply reside in the server code, in the dlls
directory.
First, go to the schedule.h
and add a new task type somewhere in the list of task types.
+TASK_GET_PATH_TO_FREEROAM_NODE,
LAST_COMMON_TASK, // LEAVE THIS AT THE BOTTOM!! (sjb)
Now we need to implement this task. Go to schedule.cpp
, in function CBaseMonster::StartTask
add the code at the end of the switch (but before the default
label!)
case TASK_GET_PATH_TO_FREEROAM_NODE:
{
if( !WorldGraph.m_fGraphPresent || !WorldGraph.m_fGraphPointersSet )
{
TaskFail();
}
else
{
for( int i = 0; i < WorldGraph.m_cNodes; i++ )
{
int nodeNumber = ( i + WorldGraph.m_iLastActiveIdleSearch ) % WorldGraph.m_cNodes;
CNode &node = WorldGraph.Node( nodeNumber );
// Don't go to the node if already is close enough
if ((node.m_vecOrigin - pev->origin).Length() < 16.0f)
continue;
TraceResult tr;
UTIL_TraceLine( pev->origin + pev->view_ofs, node.m_vecOrigin + pev->view_ofs, dont_ignore_monsters, ENT( pev ), &tr );
if (tr.flFraction == 1.0f && MoveToLocation( ACT_WALK, 2, node.m_vecOrigin ))
{
TaskComplete();
WorldGraph.m_iLastActiveIdleSearch = nodeNumber + 1;
break;
}
}
if (!TaskIsComplete())
{
TaskFail();
}
}
}
break;
Monsters perform tasks through the schedules, so we should define one. Go to schedule.h
again, and add a new schedule type to the list of schedule tasks.
+SCHED_FREEROAM,
LAST_COMMON_SCHEDULE // Leave this at the bottom
The schedule type is declared. Now let's add a schedule definition in the defaultai.cpp
(before the CBaseMonster::m_scheduleList
):
Task_t tlFreeroam[] =
{
{ TASK_STOP_MOVING, (float)0 },
{ TASK_WAIT_RANDOM, 0.5f },
{ TASK_GET_PATH_TO_FREEROAM_NODE, (float)0 },
{ TASK_WALK_PATH, (float)0 },
{ TASK_WAIT_FOR_MOVEMENT, (float)0 },
{ TASK_SET_ACTIVITY, (float)ACT_IDLE },
{ TASK_WAIT, 0.5f },
{ TASK_WAIT_RANDOM, 0.5f },
{ TASK_WAIT_PVS, (float)0 },
};
Schedule_t slFreeroam[] =
{
{
tlFreeroam,
ARRAYSIZE( tlFreeroam ),
bits_COND_NEW_ENEMY |
bits_COND_LIGHT_DAMAGE |
bits_COND_HEAVY_DAMAGE |
bits_COND_HEAR_SOUND,
bits_SOUND_DANGER,
"Free Roaming"
},
};
Here we defined a list of tasks the schedule will use, including the new TASK_GET_PATH_TO_FREEROAM_NODE
we added earlier. It has some TASK_WAIT
and TASK_WAIT_RANDOM
tasks in it, so monster will wait a bit before proceeding to the next node. You can change these parameters as you want. We also defined the schedule itself, referencing the list of tasks and setting the condititions that can interrupt the schedule (seeing an enemy, getting damage or hearing a danger sound, e.g. grenade before detonation).
You can also add slFreeroam
to the CBaseMonster::m_scheduleList
, but it's not necessary.
There's still no connection between SCHED_FREEROAM
schedule type and slFreeroam
schedule definition. Go to the CBaseMonster::GetScheduleOfType
function and add the code at the end of the big switch (but before the default
label!):
case SCHED_FREEROAM:
{
return slFreeroam;
}
So far, so good, but nothing works yet :)
Remember we were talking that freeroam feature can be set globally on the map in Sven Co-op? Let's make some modifications in the worldspawn
entity before we actually make monsters to use this feature.
First, go to the world.cpp
and a new flag for world:
#define SF_WORLD_FORCETEAM 0x0004 // Force teams
+#define SF_WORLD_FREEROAM 0x0008 // Monsters freeroaming by default
If the flag 0x0008
is already used in your mod, choose another value (must be a power of 2, e.g. 0x0010
).
Go to the CWorld::KeyValue
function and add this code (before the call to CBaseEntity::KeyValue
):
else if ( FStrEq( pkvd->szKeyName, "freeroam" ) )
{
if (atoi( pkvd->szValue ))
{
pev->spawnflags |= SF_WORLD_FREEROAM;
}
return true;
}
Note that halflife-updated
changed the KeyValue
signature and thus it uses return true
. In original Half-Life SDK it would be pkvd->fHandled = TRUE;
Then, in cbase.h
add a new variable to CWorld
class:
static bool gFreeRoaming;
Return to world.cpp
, and at the start, after the #include
directives, add:
bool CWorld::gFreeRoaming = false;
At the end of CWorld::Precache
function add:
if ((pev->spawnflags & SF_WORLD_FREEROAM) != 0)
{
gFreeRoaming = true;
}
else
{
gFreeRoaming = false;
}
Now, we can finally move to the monsters code. Define these constants somewhere in basemonster.h
:
#define FREEROAM_MAPDEFAULT 0
#define FREEROAM_NEVER 1
#define FREEROAM_ALWAYS 2
Add a function to the CBaseMonster
class declaration:
Schedule_t* GetFreeroamSchedule();
Also add a variable to the CBaseMonster
class declaration:
short m_freeRoam;
We've done with header. Let's go to the implementation, to the monsters.cpp
.
Add a new entry in the CBaseMonster::m_SaveData
:
DEFINE_FIELD( CBaseMonster, m_freeRoam, FIELD_SHORT ),
In the CBaseMonster::KeyValue
add:
else if ( FStrEq( pkvd->szKeyName, "freeroam" ) )
{
m_freeRoam = (short)atoi( pkvd->szValue );
return true;
}
Again, the return true
part is specific to halflife-updated
. Write pkvd->fHandled = TRUE;
in original Half-Life SDK.
Go to the schedule.cpp
and add GetFreeroamSchedule
implementation (actually you can do it in monsters.cpp
, but all schedule related code is in schedule.cpp
, to let's stick to the tradition):
Schedule_t* CBaseMonster::GetFreeroamSchedule()
{
if (m_freeRoam == FREEROAM_ALWAYS)
return GetScheduleOfType( SCHED_FREEROAM );
else if (m_freeRoam == FREEROAM_MAPDEFAULT)
{
if (CWorld::gFreeRoaming) {
return GetScheduleOfType( SCHED_FREEROAM );
}
}
return NULL;
}
We've done with preparations and now we can make modifications to the monsters' behavior. Go to the CBaseMonster::GetSchedule
method and make some additions under the MONSTERSTATE_IDLE
case:
else if (FRouteClear())
{
// no valid route!
+ Schedule_t* freeroamSchedule = GetFreeroamSchedule();
+ if (freeroamSchedule)
+ return freeroamSchedule;
return GetScheduleOfType(SCHED_IDLE_STAND);
}
This way when monster has nothing to do in Idle state, the monster will try to roam on nodes. There's however another state which monster enters by some conditions, e.g. after the fight ends. So make additions under the MONSTERSTATE_ALERT
case too:
+Schedule_t* freeroamSchedule = GetFreeroamSchedule();
+if (freeroamSchedule)
+ return freeroamSchedule;
else if (HasConditions(bits_COND_HEAR_SOUND))
{
return GetScheduleOfType(SCHED_ALERT_FACE);
}
else
{
return GetScheduleOfType(SCHED_ALERT_STAND);
}
FGD preparation
Before we proceed to testing, let's make some changes to the fgd
of your mod.
Add a new parameter for the worldspawn
entity:
freeroam(choices) : "Roaming monsters (node graph)" =
[
0 : "No"
1 : "Yes"
]
Add a new parameter for the Monster BaseClass
.
freeroam(Choices) : "Monster Roaming (nodes)" =
[
0 : "Map Default"
1 : "Never"
2 : "Always"
]
These definitions are from svencoop.fgd
and we implemented the feature to be compatible with Sven Co-op.
Testing
Make a map, place some nodes (info_node
) and some monsters (for testing purposes they should be prisoners or ally to the player, or you put them behind the glass wall to observe). First, let's test the global free roaming. Open worldspawn
properties (called Map properties
in some editors) and set Roaming monsters
to Yes
. Compile the map and run in the game. Monsters will walk go between nodes.
Then, let's set some monsters to never roam. Open some monster's properties and set Monster Roaming
to Never
. This time all monsters except for those you specifically set not to roam will walk between nodes.
Open Map properties
again and set Roaming monsters
to No
. Set some monsters' Monster Roaming
to Always
. Only these monsters will roam now.