MONSTERSTATE_NONE
, MONSTERSTATE_IDLE
, MONSTERSTATE_COMBAT
, MONSTERSTATE_ALERT
, MONSTERSTATE_HUNT
, MONSTERSTATE_PRONE
, MONSTERSTATE_SCRIPT
, MONSTERSTATE_PLAYDEAD
, and MONSTERSTATE_DEAD
. These are all pretty self-explanatory except for Script and Prone. Prone is done when barnacles are eating their victims. Script is for Scripted Sequences and stuff.SCHED_MELEE_ATTACK1
and SCHED_MELEE_ATTACK2
). SCHED_COMBAT_FACE
tells the NPC to look at something. SCHED_COWER
is something that the Scientist does a lot (after this tutorial, you can change that, though), and SCHED_TAKE_COVER_FROM_ENEMY
, SCHED_TAKE_COVER_FROM_BEST_SOUND
and SCHED_TAKE_COVER_FROM_ORIGIN
are things all NPCs do (all self explanatory.) There's also SCHED_RELOAD
for Marines.NOTURN
under each subtype. NPCs use a GET_PATH
system to understand where to go, while they use TASK_RUN_PATH
or TASK_WALK_PATH
to get there. Use TASK_WAIT to tell an NPC to do the next task AFTER the RUN
or WALK_PATH
tasks. Okay, the introduction is over. Up next, I'll tell you how to add a basic NPC.
#include "extdll.h"
#include "util.h"
#include "cbase.h"
#include "monsters.h"
#include "schedule.h"
#include "decals.h"
The top 5 are necessary. The last is just for this guy - it allows you, obviously, to make decals./* */
to create a block of comments (with all the contents in between) and use //
to create a line of code (with the comments AFTER the //
)//=========================
//--Monster's Anim Events--
//=========================
#define PDRONE_FIRE_SPIKE ( 1 )
#define PDRONE_MELEE_LEFT ( 3 )
#define PDRONE_MELEE_RIGHT ( 8 )
#define PDRONE_MELEE_BOTH ( 6 )
//=========================
//--Monster's Extra Defs---
//=========================
//-Body Groups
#define BODYGROUP_BODY 1
#define BODYGROUP_SPIKES 2
//-Spike Groups
#define BODY_NO_SPIKES 0
#define BODY_SIX_SPIKES 1
#define BODY_FIVE_SPIKES 2
#define BODY_FOUR_SPIKES 3
#define BODY_THREE_SPIKES 4
#define BODY_TWO_SPIKES 5
#define BODY_ONE_SPIKES 6
The first 4 lines are animation events. In animation, there are certain frames that trigger code-side eventes. The numbers are the "names" of these events, while the PDRONE_ stuff is the thing we reference the numbers by, to make it easier for us to edit.class CPitDrone : public CBaseMonster
{
public:
void Spawn( void );
void Precache( void );
int iSpikes;
int Classify ( void );
void SetYawSpeed( void );
};
Class is obvious, and so is CPitDrone. Then it says that we're taking the base as CBaseMonster. Next, we say that our public definitions (for other classes to edit) are spawn and precache. Precache means that the model is preloaded, so it doesn't make the game slower. There's a limit to 512 precaches. iSpikes is the current amount of spikes loaded into him...WRITE NOTHING IN THE COMMENTS ABOUT THAT LINE. Classify makes him choose what faction he wants to join, and SetYawSpeed changes how fast he turns.LINK_ENTITY_TO_CLASS( monster_pitdrone, CPitDrone );
Now you can use monster_pitdrone as an entity. Editing the FGD will come later. For now, you can't use "SmartEdit".void CPitDrone :: Spawn()
{
Precache( ); // So the model loads
SET_MODEL(ENT(pev), "models/npcs/pit_drone.mdl"); // So you can see him
UTIL_SetSize(pev, VEC_HUMAN_HULL_MIN, VEC_HUMAN_HULL_MAX); // Let's make his size human. If you're smart enough (or have lots of patience) you can get replace the VEC_ stuff with "Vector( x, y, z)".
pev->solid = SOLID_SLIDEBOX; // They see me slidin', they hating. This actually tells the engine for it to be Solid. Snakes can GTHO.
pev->movetype = MOVETYPE_STEP; // 'cause monsters walk - they don't drive (Nightmares will follow)
m_bloodColor = BLOOD_COLOR_GREEN; // Green blood - just like this comment. Freaked out much? (The blood's actually yellow, though)
pev->health = 100; // Health - let's keep it as an integer, as opposed to a changeable variable for now.
pev->view_ofs = Vector ( 0, 0, 20 ); // Eyes' offset (He sees you doing stuff you shouldn't)
m_flFieldOfView = 0.5; // How far he can see.
m_MonsterState = MONSTERSTATE_NONE; // Afet he spawns, make him sit there like an idiot, doing nothing.
MonsterInit(); // Starts the monsters AI
iSpikes = 6; // Default, he's fully loaded with spikes. AGAIN, NO PUNS YOU SICKOS!
}
I've commented everything so you can easily understand.
void CPitDrone :: Precache()
{
PRECACHE_MODEL("models/pit_drone_spike.mdl"); //Loads the model for the spike
PRECACHE_MODEL("models/npcs/pit_drone.mdl"); //Loads the NPC model in the game
// Bunch of pretty self-explanatory sounds
PRECACHE_SOUND("pitdrone/pit_drone_melee_attack1.wav");
PRECACHE_SOUND("pitdrone/pit_drone_melee_attack2.wav");
PRECACHE_SOUND("pitdrone/pit_drone_attack_spike1.wav");
PRECACHE_SOUND("pitdrone/pit_drone_eat.wav");
PRECACHE_SOUND("pitdrone/pit_drone_die1.wav");
PRECACHE_SOUND("pitdrone/pit_drone_die2.wav");
PRECACHE_SOUND("pitdrone/pit_drone_die3.wav");
PRECACHE_SOUND("pitdrone/pit_drone_hunt3.wav");
}
Okay, now you have a monster, but he has to epic voice chose a side end epic voice. There's 13 original CLASS_es, but I've added some of my own for my mod. Here, I'll use an original one. See the adding new monster factions tutorial for how to do this.
int CPitDrone :: Classify ( void )
{
return CLASS_ALIEN_MONSTER;
}
Finally, make him turn, turn, turn.
void CPitDrone :: SetYawSpeed( void ) {
pev->yaw_speed = 90;
}
Okay, you should have a monster sitting in your room...if you've added him to one.
$sequence "range" "range" fps 30 ACT_RANGE_ATTACK1 1 { event 1 11 } { event 1008 1 "pitdrone/pit_drone_attack_spike1.wav" }
If you look at the QC again, you'll see some events. These highlighted in blue. The first event is called "1" and is played at frame 11. The next is called "1008"" and is played at frame 1. This plays a sound - in this case, "pitdrone/pit_drone_attack_spike1.wav". Since this isn't a QC tutorial, I won't talk about that anymore.void HandleAnimEvent( MonsterEvent_t *pEvent );
Schedule_t *GetSchedule( void ); // Handles some schedules
After you've done that, it should look like this:
class CPitDrone : public CBaseMonster
{
public:
void Spawn( void );
void Precache( void );
int iSpikes;
int Classify ( void );
void SetYawSpeed( void );
void HandleAnimEvent( MonsterEvent_t *pEvent );
Schedule_t *GetSchedule( void ); // Handles some schedules
};
HandleAnimEvent, obviously, handles the animation events. But before we get to that, we have to start coding the projectile. At the top of the file, add this:
class CPitDroneSpike : public CBaseEntity
{
public:
void Spawn( void );
void Touch( CBaseEntity *pOther );
Vector waterSpeed;
};
LINK_ENTITY_TO_CLASS( pitdrone_spike, CPitDroneSpike );
void CPitDroneSpike :: Spawn( void )
{
Precache();
pev->solid = SOLID_SLIDEBOX;
pev->movetype = MOVETYPE_FLY;
pev->classname = MAKE_STRING( "pitdrone_spike" );
SET_MODEL( ENT(pev), "models/npcs/pit_drone_spike.mdl");
}
void CPitDroneSpike :: Touch ( CBaseEntity *pOther )
{
if ( !pOther->pev->takedamage ) {
// If the entity doesn't take damage
if (UTIL_PointContents(pev->origin) == CONTENTS_WATER)
{
pev->velocity = waterSpeed; // Go slower while in water
}
else {
pev->solid = SOLID_NOT;
pev->movetype = MOVETYPE_FLY;
pev->velocity = Vector( 0, 0, 0 );
UTIL_Sparks( pev->origin );
EMIT_SOUND( ENT(pev), CHAN_VOICE, "npcs/pitdrone/pit_drone_eat.wav", 1, ATTN_NORM );
}
}
else {
// If it does take damage
pOther->TakeDamage ( pev, pev, gSkillData.pitdroneSpikeDmg, DMG_GENERIC ); // Give damage to whatever it is
UTIL_Remove( this ); // Remove it
}
}
Everything is explained in the comments. Start a new function in the end of the .cpp file:
void CPitDrone :: HandleAnimEvent( MonsterEvent_t *pEvent ) {
Now we can start listing our animation events. This is where the earlier stated definitions come to place. Let's start a switch statement and use the anim events.
switch ( pEvent->event ) {
case PDRONE_RELOAD:
iSpikes = 6;
SetBodygroup( BODYGROUP_SPIKES, 1 );
break;
This starts a switch statement, meaning that it switches the output depending on the case. Here, we're saying that, depending on our event, we do different stuff, like the case with the PDRONE_RELOAD. It sets our loaded spikes to 6 and sets the body to match. break; means that the case has ended. case PDRONE_FIRE_SPIKE:
// Define the vectors - an offset and a direction
Vector vecspikeOffset;
Vector vecspikeDir;
// This stores pev->angles into 3 vectors, v_forward, v_up, and v_right, so we can use them in offsets.
UTIL_MakeVectors ( pev->angles );
// Move the origin to a relative offset
vecspikeOffset = ( gpGlobals->v_forward * 22 + gpGlobals->v_up * 40 );
// Now make the origin absolute, by adding the monster's origin
vecspikeOffset = ( pev->origin + vecspikeOffset );
// Setting the Direction, by taking the enemy's origin and view offset (so we hit him in his head, not his feet) and the spike offset, and then normalizing it, so 1 is the maximum.
vecspikeDir = ( ( m_hEnemy->pev->origin + m_hEnemy->pev->view_ofs ) - vecspikeOffset ).Normalize();
// Randomizing the Direction up a bit, so he's not a perfect shot.
vecspikeDir.x += RANDOM_FLOAT( -0.01, 0.01 );
vecspikeDir.y += RANDOM_FLOAT( -0.01, 0.01 );
vecspikeDir.z += RANDOM_FLOAT( -0.01, 0.01 );
// Create the spike, place it and turn it.
CPitDroneSpike *pSpike = (CPitDroneSpike *)CBaseMonster::Create( "pitdrone_spike", vecspikeOffset, pev->angles, edict() );
// Actually setting the velocity. This is why we normalized it, so we can have different speeds.
pSpike->pev->velocity = vecspikeDir * 900;
// Same for water velocity
pSpike->waterSpeed = vecspikeDir * 300;
// Remember the pitdrone, so we can say who killed the enemy
pSpike->pev->owner = ENT(pev);
// No friction FTW
pSpike->pev->friction = 0;
// Set the angles to correspond to the velocity
pSpike->pev->angles = UTIL_VecToAngles(pSpike->pev->velocity);
// Take a spike out
iSpikes--;
// Set the body to match the spikes.
if (iSpikes == 0)
{
SetBodygroup( BODYGROUP_SPIKES, 0 );
}
else
{
SetBodygroup( BODYGROUP_SPIKES, GetBodygroup( BODYGROUP_SPIKES )+1 );
}
break;
I commented on everything, so you know what it does. case PDRONE_MELEE_LEFT:
// Only gonna comment on this one, cuz the rest are basically the same.
// This gets the enemy and attacks at the same time.
// The parameters after CheckTraceHullAttack are distance, amount of damage, and type
CBaseEntity *pHurt = CheckTraceHullAttack( 85, 20, DMG_SLASH );
// If you did hurt someone...
if ( pHurt )
{
// ...make him change his view angle a bit (only players)...
pHurt->pev->punchangle.y = 15;
pHurt->pev->punchangle.x = 8;
// ... and push him back a bit.
pHurt->pev->velocity = pHurt->pev->velocity + gpGlobals->v_up * -100;
}
break;
case PDRONE_MELEE_RIGHT:
CBaseEntity *pHurt = CheckTraceHullAttack( 85, 20, DMG_SLASH );
if ( pHurt )
{
pHurt->pev->punchangle.y = -15;
pHurt->pev->punchangle.x = 8;
pHurt->pev->velocity = pHurt->pev->velocity + gpGlobals->v_up * -100;
}
break;
case PDRONE_MELEE_BOTH:
CBaseEntity *pHurt = CheckTraceHullAttack( 85, 30, DMG_SLASH );
if ( pHurt )
{
pHurt->pev->punchangle.x = 15;
pHurt->pev->velocity = pHurt->pev->velocity + gpGlobals->v_up * -100;
}
break;
I still commented on it. Remember, this is for all three of his melees - left, right, and both, because we changed up his angles a bit. Read the comments. Let's close off the switch statement now. Add this right afterwards.
default:
CBaseMonster::HandleAnimEvent( pEvent );
break;
}
}
What that, basically, did was check to see if the base has any events to use, and closes the switch and function.GetSchedule
function:
Schedule_t* CPitDrone::GetSchedule(void)
{
// Call another switch class, to check the monster's attitude
switch (m_MonsterState)
{
// Manly monster needs to fight
case MONSTERSTATE_COMBAT:
if (HasConditions(bits_COND_ENEMY_DEAD))
{
// The enemy is dead - call base class, all code to handle dead enemies
// is
// centralized there.
return CBaseMonster::GetSchedule();
}
// Can I attack melee style?
if (HasConditions(bits_COND_CAN_MELEE_ATTACK1))
{
// Randomize my melee attacks, so it's a bit different
switch (RANDOM_LONG(0, 1))
{
case 0:
return GetScheduleOfType(SCHED_MELEE_ATTACK1);
break;
case 1:
return GetScheduleOfType(SCHED_MELEE_ATTACK2);
break;
}
}
// I can range attack! HELLZ YEAH!
if (HasConditions(bits_COND_CAN_RANGE_ATTACK1))
{
// TOO CLOSE! USE MELEE.
if ((pev->origin - m_hEnemy->pev->origin).Length() <= 256)
{
return GetScheduleOfType(SCHED_CHASE_ENEMY);
}
if ((pev->origin - m_hEnemy->pev->origin).Length() <= 512)
{
// Do I have spikes?
if (pev->body != BODY_NO_SPIKES)
{
// Yes. Fire!
return GetScheduleOfType(SCHED_RANGE_ATTACK1);
}
else
{
// No.
if ((pev->origin - m_hEnemy->pev->origin).Length() <= 312)
{
// I'm close, I can go and attack.
if (HasConditions(bits_COND_CAN_MELEE_ATTACK1))
{
switch (RANDOM_LONG(0, 1))
{
case 0:
return GetScheduleOfType(SCHED_MELEE_ATTACK1);
break;
case 1:
return GetScheduleOfType(SCHED_MELEE_ATTACK2);
break;
}
}
// Lemme get a bit closer, so I can attack
else
{
return GetScheduleOfType(SCHED_CHASE_ENEMY);
}
}
// He's too far to melee. I'll just reload
else
{
return GetScheduleOfType(SCHED_RELOAD);
}
}
}
// Too far to either fire the spikes or melee, so lemme just get closer,
// so I'm more accurate.
else
{
return GetScheduleOfType(SCHED_CHASE_ENEMY);
}
}
// If I can do nothing, just chase after him
return GetScheduleOfType(SCHED_CHASE_ENEMY);
break;
}
// The base probably knows what to do
return CBaseMonster::GetSchedule();
}
I've commented on the reason I put what where, but, just so you know, return GetScheduleOfType( SCHED_WHATEVER );
returns the schedule, so he can know what to do.virtual BOOL CheckRangeAttack1( float flDot, float flDist );
virtual BOOL CheckRangeAttack2( float flDot, float flDist );
virtual BOOL CheckMeleeAttack1( float flDot, float flDist );
virtual BOOL CheckMeleeAttack2( float flDot, float flDist );
That work pretty much the same. They check whether the monster is allowed to range or melee or not.BOOL CBaseMonster :: CheckMeleeAttack2 ( float flDot, float flDist ) {
if ( flDist <= 64 && flDot >= 0.7 )
{
return TRUE;
}
return FALSE;
}
That basically checks if the flDist (Distance, obviously), and flDot (I believe that's how-in-sight he is).
Schedule_t* CBaseMonster :: GetScheduleOfType ( int Type )
{
switch ( Type )
{
case SCHED_CHASE_ENEMY:
return &slChaseEnemy[ 0 ];
default:
ALERT ( at_console, "GetScheduleOfType()nNo CASE for Schedule Type %d!n", Type );
return &slIdleStand[ 0 ];
break;
}
return NULL;
}
In the example, you get the Type from the GetSchedule, where the function is called from. You put it into a switch statement. If it's SCHED_CHASE_ENEMY
, release the proper schedule, in this case, &slChaseEnemy[ 0 ]. Otherwise, send an alert at the console, and give the standing schedule. If even THAT doesn't work, return nothing at all (that should never happen).
CUSTOM_SCHEDULES
in the definition:
...
void Precache( void );
CUSTOM_SCHEDULES;
};
Now, go on to the schedule names.
enum
{
SCHED_GET_BEST_ITEM = LAST_TALKMONSTER_SCHEDULE + 1,
SCHED_COVER_AND_GET_ITEM,
LAST_HEVSCIENTIST_SCHEDULE, // MUST be last
};
That's an example from my HEV scientists (who can fight back and pick up weapons). LAST_COMMON_SCHEDULE
can replace LAST_TALKMONSTER_SCHEDULE
if you're using CBaseMonster
instead of a Talking Monster. LAST_HEVSCIENIST_SCHEDULE
should be renamed to fit your own monster, and the two schedules (SCHED_GET_BEST_ITEM
and SCHED_COVER_AND_GET_ITEM
) could be changed. The second can be removed or duplicated. The ` = LAST_TALKMONSTER_SCHEDULE + 1,` and LAST_HEVSCIENTIST_SCHEDULE,
NEED to be there, but the text depends on the monster.DEFINE_CUSTOM_SCHEDULES( CHEVScientist )
{
slGetBestItem,
slCoverAndGetItem,
};
IMPLEMENT_CUSTOM_SCHEDULES( CHEVScientist, CTalkMonster );
The CHEVScienist
s should be changed to match your class, and the sl
-s should match something that resembles your SCHED_s. The IMPLEMENT_CUSTOM_SCHEDULES
is necessary, and, again, the values should be substituted for your class and base respectively.Task_t tlHeal[] =
{
{ TASK_MOVE_TO_TARGET_RANGE, (float) 50 }, // Move within 60 of target ent (client)
{ TASK_SET_FAIL_SCHEDULE, (float) SCHED_TARGET_CHASE }, // If you fail, catch up with that guy! (change this to put syringe away and then chase)
{ TASK_FACE_IDEAL, (float) 0 },
{ TASK_SAY_HEAL, (float) 0 },
{ TASK_PLAY_SEQUENCE_FACE_TARGET, (float) ACT_ARM }, // Whip out the needle
{ TASK_HEAL, (float) 0 }, // Put it in the player
{ TASK_PLAY_SEQUENCE_FACE_TARGET, (float) ACT_DISARM }, // Put away the needle
};
Schedule_t slHeal[] =
{
{
tlHeal,
ARRAYSIZE ( tlHeal ),
0, // Don't interrupt or he'll end up running around with a needle all the time
0,
"Heal"
},
};
A LOT of stuff going on here. First of all, this is the heal schedule, and, again, replace everything you need to. From the top, we started the task list. This part is pretty obvious. We type up the different tasks we're gonna use. For example, the first one: TASK_MOVE_TO_TARGET_RANGE
says the task - that he should get close to the target (the player), and (float)50
defines the parameter - the distance. This is not always necessary, and you should use (float) 0
when nothing else is necessary. You can set different schedules and different acts, too.tlHeal
is the list, and ARRAYSIZE( tlHeal )
says the size of the list. The next two variables are the interruption conditions, and the other one is the "sound" interruption conditions. "Heal" is the name of the schedule.
enum
{
TASK_GET_PATH_TO_BEST_ITEM = LAST_TALKMONSTER_TASK + 1,
TASK_PICK_UP_ITEM,
LAST_HEVSCIENTIST_TASK, // MUST be last
};
To say what it does, use this in the class:
void StartTask( Task_t *pTask );
Now, you can create the function itself:
void CHEVScientist :: StartTask( Task_t *pTask )
{
switch ( pTask->iTask )
{
case TASK_RELOAD:
m_IdealActivity = ACT_RELOAD;
break;
default:
CTalkMonster::StartTask( pTask );
}
}
Basically, it takes the task through a switch, and either calls the baseclass if it's not found, or does what's said - calling an ACT_
.You must log in to post a comment. You can login or register a new account.
Thanks to all the commenters!
I use this tutorial and is very good but i have only 3 "errors":
First, the pitdrone doesn´t fire.
Second, the melee attack left and right doesn´t make damage to the player.
and finally, when the pitdrones kill the player, using the attack "double", and i press a key to automatically restore the game i have this message error:
"ED_ALLOC = No edits yet."
**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--**--
Ok, for the first, i dont need them for the moment, my new monster doesn´t fire, so i think if i need that in the future, i can use the bullchicken code, or whatever.
For the second, i dont see where is the error...if anyone can help me.
For the last one, ("the most important"), i read about the "edits", they are the "slots" for the monsters, all hl monsters have one...so, ¿you need to "define" a slot for the pitdrone? how do that? heeelp! i am so close to finish my mod...i can´t release them until i have coding at least three new npc´s....
So, thanks for all, I hope someone can help me.
default:
CBaseMonster::HandleAnimEvent( pEvent );
break;
}
} "
Right after what exactly??
Before I got to the reload part, everything compiled fine.
But, every time the I enter the pit drone's FOV, the game exits to the main menu, literally disconnecting me from the game.
The integers that represent the animation events were not the same as what was written in the .qc for the melee attacks and reload so you had to change it to this
#define PDRONE_FIRE_SPIKE ( 1 )
#define PDRONE_MELEE_LEFT ( 2 )
#define PDRONE_MELEE_RIGHT ( 4 )
#define PDRONE_MELEE_BOTH ( 6 )
#define PDRONE_RELOAD ( 7 )
the return NULL under GetScheduleOfType was causing crashes so i changed it to:
return CBaseMonster::GetScheduleOfType(Type);
i had to make a custom checkAmmo function and override checkRangeAttack1 and explicitly call them in getSchedule.
and thats about it. heres a video preview of the changes i made and the showcasing it working. I will share the gitHub Code below so you can reference it.
https://github.com/TylerMaster/ICE1/blob/BRANDON_SMITH_Ex1/wip_monster.cpp
https://github.com/TylerMaster/ICE1/blob/BRANDON_SMITH_Ex1/wip_monster.h