Adding a New NPC

Unless you are happy using just the selection of AI controlled characters provided by the Dark Conjunction mod, you will want to add some new characters of your own. This tutorial will look at all the steps required to accomplish that.

Type of AI Controlled Character

Because the Dark Conjunction NPCs are not scripted, their behaviour must be hard coded into the source code. This places certain limits on how the character acts unless we wish to rewrite large portions of the source. For the purposes of this tutorial we will be creating a fairly simple NPC with a limited AI. The character to be created is a "suicide civilian" whose main purpose is to walk around the level following a set of NPCPath entities. He is a noncombatant and will not attack any other character or the player. However, if he is attacked and injured (by the player or other NPCs), so his health drops below half, he will run towards his attacker and suicide bomb when close enough. A nasty surprise for anyone.

Model Requirements

The first thing we need when adding our new NPC is a model. This must be a one piece MD3 model with animations, similar to the monster models from Quake 2. The name of the MD3 model file determines where the model and all its support files must be placed. We will be using a MD3 model called man.md3 (actually a recycled Quake 2 monster model) so all the files must be placed in the models/npc/man directory. If our model was named hobbit.md3 then the correct directory would be models/npc/hobbit.

Every NPC model must have certain animations that correspond to the actions that will take place in the game. The information about these animations, such as starting frame number and fps, is provided to the program via a text file called animation.txt, which is placed in the model directory. Every NPC must have this file.

The current source code allows each NPC to have 14 different animations. The model does not have to support all these animations but all 14 must be defined in the animation.txt file, with the unsupported ones being ignored by the program. The following table describes the default animation action assigned to each of the 14 animation numbers :

Animation #
Animation Action
1
Death Animation #1
2
Death Animation #2
3
Death Animation #3
4
Taunt
5
Attack Far
6
Attack Melee
7
Standing
8
Standing Active
9
Walk
10
Run
11
Backpedal
12
Jump
13
Land
14
Pain


None of these actions are fixed and can be modified in the code so the animation number is used for other actions. For example, the Sealord uses animation #6 as an alternate far attack rather than a melee attack. For ease of coding, the Death and Pain animations should be left at their default numbers but everything else is alterable.

In the animation.txt file we must provide the following information for each animation :

  • start frame - the frame number of the start of the animation, starting from 0
  • numframes - the number of frames in the animation. A value of 0 means no animation is supported. A negative value will run the animation in reverse.
  • looping - the number of frames in an animation that loops. The last looping number of frames are repeated. A value of 0 means no looping.
  • fps - the frames per second of the animation

Each line in the file describes an animation, starting at 1 and going to 14.

For our NPC we only need a subset of the animations, as he doesn't do all the actions. Along with the three death animations (which technically aren't needed since he explodes at death) and the pain animation, our NPC must be able to walk, run, stand in place and do the melee attack that results in the suicide bomb explosion. This requires us to define the 1, 2, 3, 6, 7, 8, 9, 10 and 14 numbered animations. The animation.txt file for our NPC would look like :

// start frame, numframes, looping, frames per second
      125          20        0            10            // Death #1
      145          25        0            10            // Death #2
      170          9         0            10            // Death #3
      0            0         0            10            // no animation
      0            0         0            10            // no animation
      50           22        22           10            // melee attack
      50           22        22           10            // stand inactive
      1            49        49           10            // stand active
      72           14        11           10            // walk
      92           8         8            10            // run
      0            0         0            10            // no animation
      0            0         0            10            // no animation
      0            0         0            10            // no animation
      110          10        0            10            // pain

You will notice that in the no animation lines it doesn't matter what values we use for the start frame or the fps, as long as the numframes value is 0.

Model Sounds

Going hand in hand with the model animations are the sounds heard when those animations are played. It is not necessary to define sounds for each animation, or to have sounds at all. If you do wish to have sounds for your NPC though, they are defined for the program in a text file named sounds.txt, which is placed in the model's directory.

Each line in the file defines a sound that will be played when a specified model animation number is being used by the program. The following information is provided in each line :

  • animation number - the number of the animation associated with this sound
  • starting frame - the frame number of the animation where the sound will be played
  • sound - path and name of sound file

Since we do not have to define sounds for all animations we must tell the program when it has reached the end of the list. This is done by setting the animation number to -1 in the last line. If multiple sounds are defined for an animation then the program will randomly select which sound to use.

For our NPC the sounds.txt file would look like :

// animation number, starting frame,             sound file
        1                 0           sound/npc/man/mandeath1.wav
        2                 0           sound/npc/man/mandeath2.wav
        3                 0           sound/npc/man/mandeath2.wav
        9                 6           sound/player/footsteps/boot1.wav
        9                 13          sound/player/footsteps/boot1.wav
        9                 6           sound/player/footsteps/boot2.wav
        9                 13          sound/player/footsteps/boot2.wav
        9                 6           sound/player/footsteps/boot3.wav
        9                 13          sound/player/footsteps/boot3.wav
        10                0           sound/player/footsteps/boot1.wav
        10                4           sound/player/footsteps/boot1.wav
        10                0           sound/player/footsteps/boot2.wav
        10                4           sound/player/footsteps/boot2.wav
        10                0           sound/player/footsteps/boot3.wav
        10                4           sound/player/footsteps/boot3.wav
        14                0           sound/npc/man/manpain1.wav
        14                0           sound/npc/man/manpain2.wav
        -1

This provides us with sounds for the death, walking, running and pain animations. The last three animations have multiple sounds associated with them.

Model Configuration

When an NPC is added to the program the default values for it's configuration are hard coded into the source. These values set things such as bounding box size, starting health and movement speed. It is possible to override some of these defaults and provide different values for the NPC by using the config.txt text file, which is placed in the model's directory. This allows tweaking of the NPC without having to recompile after every change.

Each line in the config.txt defines a different configuration value. All values must be present in the file and in the correct order or nothing will work properly. The values are defined in the following order :

  • health - starting health
  • pain factor - chance of playing pain animation with 0 as never and 1 as always
  • walking speed - movement speed when walking
  • running speed - movement speed when running
  • fov - field of view in degrees
  • jump height - maximum jump height
  • walking rotation speed - speed of turning while walking in angle/frame
  • running rotation speed - speed of turning while running in angle/frame
  • min bounding box - min coords of bounding box
  • max bounding box - max coords of bounding box
  • eye coords - coords of eyes

The three coord values are defined on the line as follows :

     ( x   y   z )

where x is the x-value, y is the y-value and z is the z-value. For example the eye coords would be defined by :

     ( 0 0 36 )

The config.txt file for our NPC would look like the following :

100                // health
0.9                // pain factor
22                 // walking speed
50                 // running speed
140                // fov
20                 // jump height
10                 // walking rotation
30                 // running rotation
( -16 -16 -24 )    // min bounding box
( 16 16 40 )       // max bounding box
( 0 0 36 )         // eye coords


This completes the description of the files required for an NPC model. To summarize - you need a one piece MD3 model, the animation.txt file and, optionally, the sounds.txt and config.txt files. These are all placed in a subdirectory of models/npc whose name is the same as the name of the model.

Entity Definition

With our model resources complete, we now turn to defining the entity we will use to place our NPC into the levels. This information must be placed in the entity definition file of your level editor. The name of the entity is determined by the name of the directory that contains the model information, which in turn is determined by the name of the model. Since we are using a model named man.md3 our new entity must be named npc_man in order for the program to be able to use it. If our model had been named hobbit.md3 then the entity would be named npc_hobbit.

The following is the entity definition for the man NPC :

/*QUAKED npc_man (0.2 0.5 0) (-16 -16 -40) (16 16 8) suspended disabled deaf first_taunt
"shot_factor" likeness of NPC to fire (far weapon), 1.0 means always fire,
0 means never fire, only chase, default is 0.5
*/

He is placed in the level and has his flags and keys set exactly like any other NPC. The deaf and first_taunt flags plus the shot_factor key are ignored for this NPC.

Source Code Changes

Normally, adding a new NPC only requires changes to the game module but for this NPC we will be adding an explosion effect (for the suicide) that requires changes in the cgame module as well.

Game Module Changes

Because this is a new type of NPC we must add it to the list of NPC types. In keeping with the naming scheme already in place, our NPC's type will be NPC_MAN.

In bg_public.h about line 657 add :

	NPC_SEALORD,
NPC_SOLDIER1,
NPC_SOLDIER2,
// npc addon
NPC_MAN,
// end npc addon

NPC_NUMNPCS
} npcType_t;

We now add the definition of our NPC and its default values to the bg_npclist array. Some of these values will be used if the config.txt file is missing.

In bg_misc.c at the end of the bg_npclist definition about line 195 add :

		{0,0,30},
"", // precache
"" // sounds
},
// npc addon
{
"npc_man" // classname
NPC_MAN,
100, // health
1.0, // pain factor
20, // walk speed
60, // runningSpeed
180, // fov
20, // jumpHeight
20, // walkingRotSpd
75, // runningRotSpd
110, // melee distance
20, // melee damage
0, // far damage
{0},
{-24,-24,-24}, // bounding box min
{24,24,40}, // bounding box max
{0,0,30}, // eye coords
"", // precache
"" // sounds
},
// end npc addon

{NULL},
};

At this point we have a new NPC defined in our program. It can follow a path and will attack the player or other NPCs when it sights them. It is something of a generic monster with no special features or actions. We must now customize the AI and action code so we create our "suicide civilian" NPC.

Since we are going to use a special effect for the explosion, and this is passed to the cgame module, we must add a new event to the program.

In bg_public.h about line 455 add :

#ifdef SINGLEPLAYER // npc
EV_EARTHQUAKE,
// npc addon
EV_MANEXPLODE,
// end npc addon

#endif

Other NPCs react to injury (pain) differently than our new NPC. This NPC ignores the injury (except for the pain animation being shown) until its health drops below 50%. Then it attacks whoever inflicted the injury.

In g_npc.c in function PainNPC about line 31 add :

	float NPCPainFreq=self->npc->painFreq;
float factor;
// npc addon
if(self->npc->npcType!=NPC_MAN)
{

if (attacker && // soldiers and pilot dont fight themselves
!(attacker->s.eType==ET_NPC && (attacker->npc->npcType==NPC_SOLDIER1 ||
attacker->npc->npcType==NPC_SOLDIER2 || attacker->npc->npcType==NPC_PILOT) &&
(self->npc->npcType==NPC_SOLDIER2 || self->npc->npcType==NPC_PILOT || self->npc->npcType==NPC_SOLDIER1))
&& self->ns.enemy!=&g_entities[0])
{
self->ns.command=NPC_COM_ATTACK;
self->ns.enemy=attacker;
self->ns.attackTime=0;
VectorCopy(self->ns.enemy->r.currentOrigin,self->ns.real_obj);
VectorCopy(self->ns.enemy->r.currentOrigin,self->ns.obj);
}
} else {
if (attacker && (self->health<(self->npc->health/2))) {
self->ns.command=NPC_COM_ATTACK;
self->ns.enemy=attacker;
self->ns.attackTime=0;
self->ns.toFire=0;
VectorCopy(self->ns.enemy->r.currentOrigin,self->ns.real_obj);
VectorCopy(self->ns.enemy->r.currentOrigin,self->ns.obj);
NPC_FindTarget(self);
}
}
// end npc addon

self->ns.painAcum+=damage;

This NPC also dies differently than the other NPCs. Instead of showing one of the death animations and falling to the ground, it blows up and dissappears. The explosion is created in the cgame module using the EV_MANEXPLODE event.

In g_npc.c in function DieNPC about line 87 add :

	int death;
// npc addon
gentity_t *nent;
vec3_t v;
if(self->npc->npcType==NPC_MAN)
{
nent = G_Spawn();
G_AddEvent(nent, EV_MANEXPLODE, 255);
nent->s.otherEntityNum = self->s.number;
v[0] = self->r.currentOrigin[0] + (self->r.mins[0] + self->r.maxs[0]) * 0.5;
v[1] = self->r.currentOrigin[1] + (self->r.mins[1] + self->r.maxs[1]) * 0.5;
v[2] = self->r.currentOrigin[2] + (self->r.mins[2] + self->r.maxs[2]) * 0.5;
nent->freeAfterEvent = qtrue;
nent->s.eType = ET_GENERAL;
G_SetOrigin( nent, v );
trap_LinkEntity( nent );
G_FreeEntity( self );
return ;
}
// end npc addon

self->takedamage=qfalse;
self->ns.state=NPC_ST_DEAD;

In the current NPC code the melee distance defined in the bg_npclist definition is ignored (it is set to 0 for all other NPCs anyway) and a distance determined by the bounding box size is calculated. This causes our NPC to have to get very close to it's attacker before it explodes. What we really want is to have it explode at some point after it reaches its defined melee distance. This will make the distance away when it explodes slightly more random.

In g_npc.c in function RegisterNPC about line 222 add :

	Parse1DMatrix (&text_p, 3, npc->mins);
Parse1DMatrix (&text_p, 3, npc->maxs);
Parse1DMatrix (&text_p, 3, npc->eye);
// npc addon
if(npc->melee_dist==0)
// end npc addon
npc->melee_dist=(abs(npc->maxs[0]-npc->mins[0])+abs(npc->maxs[1]-npc->mins[1]))*0.75;

read_times:

In the movable NPCs the shot_factor key determines the type or combination of attacks that they perform. You can make them do more melee or more shooting attacks by varying this key value in the entity. Our NPC has only one type of attack, the melee, and we want him to do this all the time.

In g_npc.c in function SP_npc about line 415 add :

	RegisterNPC( npc );
ent->npc = npc;
// npc addon
if (ent->npc->npcType==NPC_MAN)
ent->ns.shot_factor = 0.9f;
// end npc addon

if ( ent->spawnflags & 2 )

Other NPCs that do the melee attack have a waiting period between attacks so damage is not inflicted continuously. Again, this NPC doesn't need this delay since it only does one attck before blowing up.

In g_npc.c in function G_RunNPC about line 479 add :

		if (npc->meleeTime)
{
// npc addon
if ((npc->meleeTime <= level.time) || ent->npc->npcType==NPC_MAN)
// end npc addon

{
npc->fireTime=0;

and in g_npcthink.c in function NPC_ThinkMove about line 679 add :

						if (ent->npc->npcType==NPC_BAT && npc->inFlight)
npc->fireTime=level.time + ent->npc->animTimes[ANPC_ATTACK_MELEE]*0.8;
else
// npc addon
{
if (ent->npc->npcType==NPC_MAN)
npc->meleeTime=level.time;
else

npc->meleeTime=level.time+ent->npc->animTimes[ANPC_ATTACK_MELEE]*0.8;
}
// end npc addon

}
else
{
if (ent->npc->npcType==NPC_METLAR)

When this NPC does its suicide attack it inflicts damage on its attacker and then dies. The dying triggers the explosion, as shown above. This attack and die method is different from other NPCs so we must make the man melee attack a special case.

In g_npc.c in function G_RunNPC about line 503 change and add :

							g_entities[0].slow_event=level.time+HULK_QUAKE_LEN;
}
}
// npc addon
if (NPC_InFieldOfVision(npc->ps.viewangles,120,ang))
{
if (ent->npc->npcType==NPC_MAN)
{
if(len <ent->npc->melee_dist && ent->health>0)
{
G_Damage(npc->enemy,ent,ent,dir,npc->enemy->r.currentOrigin, ent->npc->melee_damage*(int)(1.0f+0.25f*(float)npc_skill),0,0);
G_Damage(ent,&g_entities[0],&g_entities[0],NULL, ent->r.currentOrigin,10000,0,0);
return ;
}
}
else
{
if(len<=ent->npc->melee_dist*1.2)
G_Damage(npc->enemy,ent,ent,dir,npc->enemy->r.currentOrigin, ent->npc->melee_damage*(int)(1.0f+0.25f*(float)npc_skill),0,0);
}
}
// end npc addon

}
npc->meleeTime=0;

Since this NPC doesn't check around to find something to attack we must inhibit that behaviour and we must make it so pilots and soldiers don't attack him but the other monsters do.

In g_npcthink.c in function NPC_CheckAround about line 244 add :

			if (g_entities[i].s.eType!=ET_PLAYER && g_entities[i].s.eType!=ET_NPC) continue;

// npc addon
if( npcType==NPC_MAN ) continue ; // man doesn't attack anything
// end npc addon

if (g_entities[i].s.eType==ET_PLAYER && (npcType==NPC_SOLDIER2 if (g_entities[i].s.eType==ET_NPC && ( npcType==NPC_SOLDIER
|| npcType==NPC_SOLDIER1 || npcType==NPC_PILOT )
&& (g_entities[i].npc->npcType==NPC_SOLDIER2 || g_entities[i].npc->npcType==NPC_PILOT
// npc addon
|| g_entities[i].npc->npcType==NPC_MAN // soldiers and pilots don't attack man
// end npc addon

|| g_entities[i].npc->npcType==NPC_SOLDIER1)) continue; if (g_entities[i].s.eType==ET_NPC && ( npcType!=NPC_SOLDIER2
&& npcType!=NPC_SOLDIER1 && npcType!=NPC_PILOT ) // other monsters
&& (g_entities[i].npc->npcType!=NPC_SOLDIER2 && g_entities[i].npc->npcType!=NPC_PILOT
// npc addon
&& g_entities[i].npc->npcType!=NPC_MAN // monsters attack man
// end npc addon

&& g_entities[i].npc->npcType!=NPC_SOLDIER1)) continue;

This NPC has no far attack so we will make it so he never tries to change to that animation.

In g_npcthink.c in function NPC_ThinkMove about line 699 add :

							ucmd->upmove=ent->npc->jumpHeight*0.7;
ucmd->forwardmove=127;
}
// npc addon
else if (ent->npc->npcType!=NPC_MAN)
// end npc addon

{
npc->ps.legsAnim=((npc->ps.legsAnim & ANIM_TOGGLEBIT)^ANIM_TOGGLEBIT) | ANPC_ATTACK_FAR;

When other NPCs are attacking they will sometimes stop and delay before resuming their attack. We don't want our new NPC to do this, as he is supposed to run straight in and blow up.

In g_npcthink.c in function NPC_ThinkMove about line 728 add :

					npc->attackTime=15+random()*10;
// ANK or any walking NPC
// npc addon
if (r>0.9 && npc->canStand && !npc->inFlight && ent->npc->npcType!=NPC_MAN)
// end npc addon
{
int an;

And lastly is a bug fix dealing with NPC step height. This affects all NPCs when moving over an entity.

In g_npcthink.c about line 414 change :

// npc addon
#define STEP_HEIGHT 16
// end npc addon


Cgame Module Changes

Normally, a new NPC doesn't require any changes to the Cgame module as everything is handled server side. For our new NPC though, we want to create a new explosion event when he dies. This will show a splash of blood and throw gibs around to indicate a suicide bombing took place.

In cg_local.h about line 1512 add :

#ifdef SINGLEPLAYER // entity
void CG_BlackBars(void);
// npc addon
void CG_ManExplode( vec3_t org, int entityNum );
// end npc addon

#endif

In cg_effects.c at the end of the file add :

	cg.refdefViewAngles[2]+=terremotoZ;
AnglesToAxis( cg.refdefViewAngles, cg.refdef.viewaxis );
}
// npc addon
/*
==================
CG_ManExplode
==================
*/
void CG_ManExplode( vec3_t org, int entityNum ) {
vec3_t origin;
// create an explosion
VectorCopy( org, origin );
CG_BigExplode(origin) ;
CG_GibPlayer( origin ) ;
CG_StartEarthquake(8,500);
}
// end npc addon

#endif

In cg_event.c in function CG_EntityEvent about line 469 add :

	const char		*s;
int clientNum;
clientInfo_t *ci;
#ifdef SINGLEPLAYER // npc addon
vec3_t offset, xoffset;
vec3_t v[3];
#endif

es = &cent->currentState;

In cg_event.c in function CG_EntityEvent about line 841 add :

			CG_StartEarthquake( es->eventParm&0x0F,
(1 + ((es->eventParm&0xF0)>>4))*2000);
break;
// npc addon
case EV_MANEXPLODE:
AnglesToAxis( cent->lerpAngles, v );
offset[0] = 0;
offset[1] = 0;
offset[2] = 24;
xoffset[0] = offset[0] * v[0][0] + offset[1] * v[1][0] + offset[2] * v[2][0];
xoffset[1] = offset[0] * v[0][1] + offset[1] * v[1][1] + offset[2] * v[2][1];
xoffset[2] = offset[0] * v[0][2] + offset[1] * v[1][2] + offset[2] * v[2][2];
VectorAdd( cent->lerpOrigin, xoffset, xoffset ); trap_S_StartSound( NULL, es->number, CHAN_BODY, cgs.media.gibSound );
CG_ManExplode( xoffset, es->eventParm );
break;
// end npc addon

#endif
#ifdef SINGLEPLAYER // entity
case EV_PLAYERSTOP:


And this completes our "suicide civilian" NPC. On the Downloads page is a package containing the model, support files and sounds used for this NPC. It is a slightly modified Quake 2 monster converted to md3 format and is provided only as an example.

 

Return to Home Page