Authoring a Model Entity
This tutorial assumes you have completed and understood Authoring a Logical Entity.
In this tutorial we'll create an entity that can move, collide with other objects, and that has a visual component (in this case, a model). We will make the entity randomly move around the world.
Create Server/Source Files/sdk_modelentity.cpp
and we'll begin.
Include and Declare
You should understand this code now:
#include "cbase.h" class CMyModelEntity : public CBaseAnimating { public: DECLARE_CLASS( CMyModelEntity, CBaseAnimating ); DECLARE_DATADESC(); void Spawn( void ); void Precache( void ); void MoveThink( void ); // Input function void InputToggle( inputdata_t &inputData ); private: bool m_bActive; float m_flNextChangeTime; };
Notice how we are now inheriting from CBaseAnimating
, and how we have some new functions. The private variables are a boolean (true/false) and a floating point (decimalised number).
Entity name and Datadesc
LINK_ENTITY_TO_CLASS( my_model_entity, CMyModelEntity ); // Start of our data description for the class BEGIN_DATADESC( CMyModelEntity ) // Save/restore our active state DEFINE_FIELD( m_bActive, FIELD_BOOLEAN ), DEFINE_FIELD( m_flNextChangeTime, FIELD_TIME ), // Links our input name from Hammer to our input member function DEFINE_INPUTFUNC( FIELD_VOID, "Toggle", InputToggle ), // Declare our think function DEFINE_THINKFUNC( MoveThink ), END_DATADESC()
The biggest change here from our logical entity is DEFINE_THINKFUNC
. Think functions are special cases in Source, and we need to identify them.
Defining the model
// Name of our entity's model #define ENTITY_MODEL "models/gibs/airboat_broken_engine.mdl"
This hard-codes the model that our entity will present to the world. This is a static piece of information, not a variable. It cannot change once the code has been compiled. The path to the .mdl file is relative to the game directory (e.g. /hl2/
).
Precache()
Now we come to our first function. Precache()
should be called when an entity spawns, and ensures that loading of whatever is listed in it occurs before the player spawns (i.e. sees the world and gains control). See Precaching Assets for more information.
Precaching gets a special name because there are other types of "asynchronous" loading that can occur after player spawn.
//----------------------------------------------------------------------------- // Purpose: Precache assets used by the entity //----------------------------------------------------------------------------- void CMyModelEntity::Precache( void ) { PrecacheModel( ENTITY_MODEL ); }
For this entity there is only one resource that needs precaching. More complex entities might also precache sounds, particle effects, and so on. No matter how much loading needs to be done though, this is generally a very simple function.
Spawn()
You may have noticed the absence of a constructor from the class declaration. That is because we're now using Valve's Spawn()
function instead. Like a constructor it gets called every time an instance is made, but it does some Source-specific things as well.
//----------------------------------------------------------------------------- // Purpose: Sets up the entity's initial state //----------------------------------------------------------------------------- void CMyModelEntity::Spawn( void ) { Precache(); SetModel( ENTITY_MODEL ); SetSolid( SOLID_BBOX ); UTIL_SetSize( this, -Vector(20,20,20), Vector(20,20,20) ); m_bActive = false; }
We call Precache()
first of course, and then call several CBaseAnimating
functions.
SetSolid()
defines the shape of our entity when considering movement. It doesn't define hitboxes or affect physics collisions. These are all different things! We have several options to choose from:
SOLID_NONE
- Not solid.
SOLID_BSP
- Uses the BSP tree to determine solidity (used for brush models)
SOLID_BBOX
- Uses an axis-aligned bounding box.
SOLID_CUSTOM
- Entity defines its own functions for testing collisions.
SOLID_VPHYSICS
- Uses a .mdl's embedded collision model to test accurate physics collisions.
These are engine-level options that mod authors cannot change or add to. We will choose SOLID_BBOX
, a generic box shape that we then mould to the approximate size of our model with UTIL_SetSize
. This is cheap and simple, which is what we're after for this tutorial.
Lastly, we use m_bActive
to set the entity to default to inactivity.

Spawn()
is called immediately after the creation of the entity. If this has occurred at the beginning of a map there is no guarantee that other entities have been spawned yet. Therefore, any code which requires the entity to search or otherwise link itself to other entities is unreliable. Use the Activate()
function instead, which is always called after all spawning has completed.MoveThink()
A Think function allows an entity to make decisions without being prompted by an external source. If you think back to our logical entity, it only ever did anything when it received an input; this is clearly no good for something that is meant to move around of its own accord. A think function, if present, is usually the core of the entity's programming.
Here we create a think function that will be called up to 20 times a second. This may sound like a lot, but modern processors are very fast and this entity is very simple. You'll be able to have an awful lot of CMyModelEntity
s in a map without running into any CPU issues!
//----------------------------------------------------------------------------- // Purpose: Think function to randomly move the entity //----------------------------------------------------------------------------- void CMyModelEntity::MoveThink( void ) { // See if we should change direction again if ( m_flNextChangeTime < gpGlobals->curtime ) { // Randomly take a new direction and speed Vector vecNewVelocity = RandomVector( -64.0f, 64.0f ); SetAbsVelocity( vecNewVelocity ); // Randomly change it again within one to three seconds m_flNextChangeTime = gpGlobals->curtime + random->RandomFloat( 1.0f, 3.0f ); } // Snap our facing to where we're heading Vector velFacing = GetAbsVelocity(); QAngle angFacing; VectorAngles( velFacing, angFacing ); SetAbsAngles( angFacing ); // Think every 20Hz SetNextThink( gpGlobals->curtime + 0.05f ); }
While a lot of code is packed into this function, its outcome is fairly simple. Once a random time interval has elapsed, the entity will choose a new, random direction and speed to travel at. It will also update its angles so that the model visibly faces towards the new direction. This occurs in three dimensions.
Some help:
gpGlobals->curtime
returns the time at which the code is being executed as a floating point value.Vector
s are used for movement, since they contain information about both facing and speed.SetAbsVelocity
'Sets' the 'Absolute' 'Velocity' with one.qangle
is simply an angle - a vector minus data about velocity. It's used to set facing.VectorAngles
converts a vector (velFacing
) to an angle (angFacing
). Remember that C++ is very strict about data types: you need utility functions to convert between any two.
Having done all this we call SetNextThink()
. This tells the entity when next to run its think function. Here it is set to think again in 0.05 seconds (1/20th), but that number can vary between entities. It’s important to note that failure to use SetNextThink() will cause the entity to stop thinking (which is sometimes desired).
InputToggle()
Now we come to our last function. This is an input that will toggle movement on and off.
//----------------------------------------------------------------------------- // Purpose: Toggle the movement of the entity //----------------------------------------------------------------------------- void CMyModelEntity::InputToggle( inputdata_t &inputData ) { // Toggle our active state if ( !m_bActive ) { // Start thinking SetThink( &CMyModelEntity::MoveThink ); SetNextThink( gpGlobals->curtime + 0.05f ); // Start flying SetMoveType( MOVETYPE_FLY ); // Set our next time for changing our speed and direction m_flNextChangeTime = gpGlobals->curtime; m_bActive = true; } else { // Stop thinking SetThink( NULL ); // Stop moving SetAbsVelocity( vec3_origin ); SetMoveType( MOVETYPE_NONE ); m_bActive = false; } }
This is all very straightforward. We use an if
statement to check whether or not m_bActive
is true. The exclamation mark means "not": "if m_bActive
is not true, do this". Later on, we use the else
command to specify what we want to do in any other case - which with a boolean value can only be if m_bActive
is true.
When activating the entity we start its think loop by telling Source what think function to use (the default is Think()
, but we don't have that here) - note the addition of an & and the omission of any parentheses in the argument. We then tell Source that the entity will move by flying, although this would actually be better-described as "floating" since there is no true simulation of flight in Source (perhaps you could make one?). And, of course, we set m_bActive
to true. It isn't going to change itself!
Under the else
command we stop the entity from moving. The think function is set to NULL
to stop all thinking, AbsVelocity
is set to the entity's origin, a vector with no movement, the movement type is set to MOVETYPE_NONE
to prevent any kind of movement that might be imposed externally, and lastly m_bActive
is made false.
FGD entry
The FGD entry for this entity displays the model in Hammer and allows you to name it and send the input "Toggle".
@PointClass base(Targetname) studio("models/gibs/airboat_broken_engine.mdl")= my_model_entity : "Tutorial model entity." [ input Toggle(void) : "Toggle movement." ]
The working entity
Play with your entity. You can use the console command ent_fire my_model_entity toggle
to get it moving without map I/O. You'll notice a few things:
- If I spawn it flush against the ground, it will rotate but not move
- The bounding box goes inside the BSP, perhaps? Create it in the air and there won't be any problems.
- It won't collide with any physics objects (but it will with me)
- The model we're using has a collision mesh, but perhaps we haven't loaded it?
- It will dodge around me every time I get in its way
- Perhaps
CBaseAnimating
thinks every time its path is blocked?