Features
Home
Overview
Forums
Articles
 Mad Scientist Tutorial
 Precaching
 Activities and Sequences
 Entity Variables
 Engine Functions
 Good Half-Life Net Play
Challenges
Questions and Answers

While we're producing these pages, we'll provide a simple tutorial in the form of a simple new entity -- the mad_scientist entity. This was written by Kate and documented by Damyan.


File: madscientist.cpp

All the Mad Scientist code is in madscientist.cpp. This needs to be added to the project in Visual C++ before it will work. You can download it using the link above.


  #include "extdll.h"
  #include "util.h"
  #include "cbase.h"
  #include "monsters.h"
  #include "weapons.h"
  #include "soundent.h"
  #include "gamerules.h"
  #include "animation.h"
  #include "../engine/studio.h"
  
These are the includes that we always use. Some may not be used, but it doesn't really matter.

  class CMadScientist : public CBaseMonster
  {
  public:
  
CScientist inherits from CBaseMonster in order that we can use all of the nice functions that Valve have provided us with to help deal with monster entities.

    void Spawn();
This function is called when the engine creates the entity.

    void Precache();
This is called by Spawn to allow us to ensure that all models / sounds etc are available to us. See the precache page for more information on some of the peculiarities and misunderstandings in precaching.

    int Classify();
All entities should implement the Classify function. This virtual function (declared in CBaseEntity) is used by many other entities in the SDK to determine what type of entity they are dealing with. The SDK contains lookup tables to see what entities with one classification think of entities with another classification. This is what is used to stop snarks attacking other snarks.

    void SetActivity(int activity);
    int  GetActivity() { return m_Activity; }
The idea to use these functions was taken from xen.cpp. Basically, this is used to make setting activities / animation sequences easier. See the activities page for more information.

    void SetCollisionBox();
        
This is a function written by us to attempt to extract the collision box from the actual model. This will be described in more detail later.

    void EXPORT IdleThink();
        
This function is the entities 'think' function. Although there is only one here you'll find that more complex monsters will have several think functions. This also will be described later.

    int BloodColor() { return BLOOD_COLOR_RED; }
       
This is another function that comes from CBaseEntity. It is used throughout the SDK to get things to bleed and gib properly. Here we set it to be red, since it is a human.

    int m_Activity;
       
This is the variable used by GetActivity and SetActivity. It stores the current activity that the monster is performing.

  };
  
 

  LINK_ENTITY_TO_CLASS(mad_scientist_entity, CMadScientist);
       
This basically 'registers' the entity type with the engine. The first argument is the name of the entity. This is what is understood by Worldcraft.

  void CMadScientist::Spawn()
  {
    Precache();
       
This is the implementation of Spawn. Remember that this is called when a mad_scientist_entity is created. The function DispatchSpawn (in cbase.cpp) is responsible for this. The first thing Spawn does is to call Precache to ensure that all the stuff it needs is available.

    pev->movetype   = MOVETYPE_STEP;
    pev->solid      = SOLID_BBOX;
    pev->takedamage = DAMAGE_YES;
    pev->flags     |= FL_MONSTER;
    pev->health     = 80;
    pev->gravity    = 1.0;
       
The pev is being set up here. 'pev' is short for 'pointer to entity variables'. These variables are what the engine uses when dealing with the entity. See entity variables for detailed information on the pev. Here we are saying that this entity moves like a monster, is solid, takes damage, is a monster, has 80 health and normal gravity.

    SET_MODEL(ENT(pev), "models/scientist.mdl");
        
This sets the model for the entity to the one named here. Note that this model must have been correctly precached otherwise this Half-Life will crash. See precaching for more information.

    SetActivity(ACT_IDLE);
        
This calls our 'SetActivity' function to make the entity perform the idle animation.

    SetSequenceBox();
        
This calls our 'SetSequenceBox' function to set the bounding box of the entity to the bounding box provided by the model's current sequence.

    SetThink(IdleThink);
    pev->nextthink = gpGlobals->time + 1;
  }
        
This tells the engine that this entity's think function is 'IdleThink'. Then we tell the engine that this function should be called one second from now.

  void CMadScientist::Precache()
  {
    PRECACHE_MODEL("models/scientist.mdl");
  }
        
This function is called from Spawn. It tells the engine that this entity is going to require this model. If it were to emit any sounds, or use any sprites, then they should be precached here.

  int CMadScientist::Classify()
  {
    return CLASS_HUMAN_PASSIVE;
  }
        
This function is called by other classes in the SDK when they want to find out what type the entity is. We return CLASS_HUMAN_PASSIVE, since that is what a mad scientist is!

  void CMadScientist::SetActivity(int act)
  {
    int sequence = LookupActivity(act);
    if(sequence != ACTIVITY_NOT_AVAILABLE)
    {
      pev->sequence = sequence;
      m_Activity = act;
      pev->frame = 0;
      ResetSequenceInfo();
    }
  }
        
This function sets the activity of the model. See the activites page for detailed information. In brief, this function looks up a sequence for the given activity and sets the entity to perform this sequence.

  void CScientist::SetCollisionBox()
  {
    studiohdr_t *pstudiohdr;
    pstudiohdr = (studiohdr_t*)GET_MODEL_PTR( ENT(pev) );
  
    mstudioseqdesc_t *pseqdesc;
  
    pseqdesc = 
      (mstudioseqdesc_t *)((byte *)pstudiohdr 
                                      + pstudiohdr->seqindex);
        
    Vector min, max;
  
    min = pseqdesc[ pev->sequence ].bbmin;
    max = pseqdesc[ pev->sequence ].bbmax;
        
    UTIL_SetSize(pev,min,max);
  }
        
This bit of code is not very pretty! I suspect that it isn't actually required -- the bounding box would probably be hard coded. However, this queries the sequence information to get the bounding box associated with the current sequence. The entity's bounding box is then set to match.

  void CMadScientist::IdleThink()
  {
        
This is the only 'think' function that this entity has. Others may have more 'think' functions. A 'think' function is used to enable your entity to do things it is a bit of code that you can ask the engine to call for you.

    float flInterval = StudioFrameAdvance();
    DispatchAnimEvents(flInterval);
        
These two lines are copied from other entities in the SDK. My current understanding is that they allow animation events to be dispatched and for flags to be set (such as the flag that says when a sequence has finished.)

    pev->nextthink = gpGlobals->time + 1;
        
This tells the engine to next call our think routine in one seconds time.

    if(!IsInWorld())
    {
      SetTouch(NULL);
      UTIL_Remove(this);
      return;
    } 
        
This is a simple check that Valve, and therfore I, do in all think functions. It checks that this entity is still in the world (ie map). If it isn't then this entity is removed.

    if(pev->deadflag != DEAD_DEAD)
    {
        
This think function caters for dealing with both dead and alive scientists. The first part of the 'if' statement will deal with the alive scientist, since they have to do more.

      SetCollisionBox();
        
Do our best to ensure the bounding box is correct.

      int i;
      CBaseEntity *pNearestPlayer = NULL;
      float nearestdistance=1000;
      for(i=1; i<=gpGlobals->maxClients; i++)
      {
        CBaseEntity *pPlayer = UTIL_PlayerByIndex(i);
        if(!pPlayer)
          continue;
  
        float distance = 
           (pPlayer->pev->origin - pev->origin).Length();
  
        if(distance < nearestdistance)
        {
          nearestdistance=distance;
        pNearestPlayer=pPlayer;
        }
      } 
        
Here, we loop through all the players that the engine knows about and set pNearestPlayer to point to that player. If this doesn't make sense to you then please look at it a little closer. If it still doesn't make sense then email Damyan and I'll add some more explanation to this section.

      if(pNearestPlayer)
      {
        
This check is here in case we couldn't find a player.

        Vector toplayer = 
          pNearestPlayer->pev->origin - pev->origin;
        
toplayer is a vector that gives us the direction from the scientist to the player.

        pev->angles=UTIL_VecToAngles(toplayer);
        pev->angles.x=0;
        pev->angles.z=0;
      }
    }
        
This makes the scientist rotate so that he is 'looking' at the player. We set the x and z values of the angles to 0 so that he only rotates around his y axis. For some interesting, if a little odd, effects try commenting out the last two lines.

    else // The scientist is dead
    {
      UTIL_SetSize(pev,Vector(0,0,0),Vector(0,0,0));
    }
  }
        
If the scientist is dead then we just set his bounding box to be 0 so that he doesn't block things.

Getting the entity into your game

There are two ways of getting the entity into your game. The simplest, but most time consuming way, it to create a map and to place the mad_scientist_entity into the map as you would any other monster. The way we prefer to do it is to bind the creation of a scientist to the impulse command. This is done as shown below:

File: player.cpp

Open up the file player.cpp and find the function ImpulseCommands. Then add, immediately after the 'case 204' line add your own entry. We chose '206' Here is our code:


         case 206:
            ALERT(at_console,"Creating mad_scientist_entity\n");
          UTIL_MakeVectors(Vector(0,pev->v_angle.y,0));
          Create("mad_scientist_entity",
                   pev->origin + gpGlobals->v_forward * 128, 
                   pev->angles);
            break;
  
This just causes your entity to be created a little in front of you whenever you do 'impulse 206' in the console. We normally bind a key to this.

Now try this. Does it work? I expect it won't; Half-Life has probably crashed with an error such as 'Bad pvar in edict' or something similar (Since I've spent a lot of time getting rid of these errors, I can't remember what they are anymore!).

File: client.cpp

What we need to do is to ensure that our model gets precached before the 3D engine is started. To do this we need to add some lines to the function 'ClientPrecache' in the file 'client.cpp'. Anywhere you like in this function (we choose the end) add some lines such as the following:


      // Kate's Mad Scientist
      PRECACHE_MODEL("models/scientist.mdl");
    

And now everything should work fine. As an exercise to the reader you could try to make the scientist die properly (at the moment you can only gib him; shooting him makes a lot of blood though!) I'll probably cover this later. It is quite easy though, the SDK provides many functions to help with this sort of thing.

Still having problems?

Ok. Make sure that you're at a stage where you can actually compile and use the DLLs you want. There are some beginner tutorials around that might help with this. If you think the problem is really with the code we've provided then mail me at Damyan and I'll try to help as soon as I can.