Difference between revisions of "Authoring a Model Entity"

From Valve Developer Community
Jump to: navigation, search
(Precache(): Fixed links on PrecacheParticleSystem and PrecacheScriptSound to point to our new articles!)
(syntax highlighting)
Line 11: Line 11:
 
You should understand this code now:
 
You should understand this code now:
  
<span style="color:blue;">#include</span> <span style="color:brown;">"cbase.h"</span>
+
<syntaxhighlight lang="cpp">
+
#include "cbase.h"
<span style="color:blue;">class</span> CMyModelEntity : <span style="color:blue;">public</span> CBaseAnimating
+
 
{
+
class CMyModelEntity : public CBaseAnimating
<span style="color:blue;">public</span>:
+
{
DECLARE_CLASS( CMyModelEntity, CBaseAnimating );
+
public:
DECLARE_DATADESC();
+
DECLARE_CLASS( CMyModelEntity, CBaseAnimating );
+
DECLARE_DATADESC();
CMyModelEntity()
+
 
{
+
CMyModelEntity()
m_bActive = <span style="color:blue;">false</span>;
+
{
}
+
m_bActive = false;
+
}
<span style="color:blue;">void</span> Spawn( <span style="color:blue;">void</span> );
+
 
<span style="color:blue;">void</span> Precache( <span style="color:blue;">void</span> );
+
void Spawn( void );
+
void Precache( void );
<span style="color:blue;">void</span> MoveThink( <span style="color:blue;">void</span> );
+
 
+
void MoveThink( void );
<span style="color:green;">// Input function</span>
+
 
<span style="color:blue;">void</span> InputToggle( inputdata_t &inputData );
+
// Input function
+
void InputToggle( inputdata_t &inputData );
<span style="color:blue;">private</span>:
+
 
+
private:
<span style="color:blue;">bool</span> m_bActive;
+
 
<span style="color:blue;">float</span> m_flNextChangeTime;
+
bool m_bActive;
};
+
float m_flNextChangeTime;
 +
};
 +
</syntaxhighlight>
  
 
Notice how we are now inheriting from <code>[[CBaseAnimating]]</code>, and how we have some new functions. The private variables are a boolean (true/false) and a floating point (decimalised number).
 
Notice how we are now inheriting from <code>[[CBaseAnimating]]</code>, and how we have some new functions. The private variables are a boolean (true/false) and a floating point (decimalised number).
Line 44: Line 46:
 
== Entity name and Datadesc ==
 
== Entity name and Datadesc ==
  
LINK_ENTITY_TO_CLASS( my_model_entity, CMyModelEntity );
+
<syntaxhighlight lang="cpp">
+
LINK_ENTITY_TO_CLASS( my_model_entity, CMyModelEntity );
<span style="color:green;">// Start of our data description for the class</span>
+
 
BEGIN_DATADESC( CMyModelEntity )
+
// Start of our data description for the class
+
BEGIN_DATADESC( CMyModelEntity )
<span style="color:green;">// Save/restore our active state</span>
+
DEFINE_FIELD( m_bActive, FIELD_BOOLEAN ),
+
// Save/restore our active state
DEFINE_FIELD( m_flNextChangeTime, FIELD_TIME ),
+
DEFINE_FIELD( m_bActive, FIELD_BOOLEAN ),
+
DEFINE_FIELD( m_flNextChangeTime, FIELD_TIME ),
<span style="color:green;">// Links our input name from Hammer to our input member function</span>
+
 
DEFINE_INPUTFUNC( FIELD_VOID, "Toggle", InputToggle ),
+
// Links our input name from Hammer to our input member function
+
DEFINE_INPUTFUNC( FIELD_VOID, "Toggle", InputToggle ),
<span style="color:green;">// Declare our think function</span>
+
 
DEFINE_THINKFUNC( MoveThink ),
+
// Declare our think function
+
DEFINE_THINKFUNC( MoveThink ),
END_DATADESC()
+
 
 +
END_DATADESC()
 +
</syntaxhighlight>
  
 
The biggest change here from our logical entity is <code>DEFINE_THINKFUNC</code>. [[Think]] functions are special cases in Source, and we need to identify them.
 
The biggest change here from our logical entity is <code>DEFINE_THINKFUNC</code>. [[Think]] functions are special cases in Source, and we need to identify them.
Line 65: Line 69:
 
== Defining the model ==
 
== Defining the model ==
  
<span style="color:green;">// Name of our entity's model</span>
+
<syntaxhighlight lang="cpp">
<span style="color:blue;">#define</span> ENTITY_MODEL <span style="color:brown;">"models/gibs/airboat_broken_engine.mdl"</span>
+
// Name of our entity's model
 +
#define ENTITY_MODEL "models/gibs/airboat_broken_engine.mdl"
 +
</syntaxhighlight>
  
 
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. <code>hl2/</code>).
 
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. <code>hl2/</code>).
Line 78: Line 84:
 
Precaching gets a special name because there are other types of "[[asynchronous]]" loading that can occur ''after'' player spawn.
 
Precaching gets a special name because there are other types of "[[asynchronous]]" loading that can occur ''after'' player spawn.
  
<span style="color:green;">//-----------------------------------------------------------------------------
+
<syntaxhighlight lang="cpp">
// Purpose: Precache assets used by the entity
+
//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------</span>
+
// Purpose: Precache assets used by the entity
<span style="color:blue;">void</span> CMyModelEntity::Precache( <span style="color:blue;">void</span> )
+
//-----------------------------------------------------------------------------
{
+
void CMyModelEntity::Precache( void )
PrecacheModel( ENTITY_MODEL );
+
{
+
PrecacheModel( ENTITY_MODEL );
BaseClass::Precache();
+
 
}
+
BaseClass::Precache();
 +
}
 +
</syntaxhighlight>
  
 
For this entity we precache the model we will be using, then call the underlying precache function of our base class (which in <code>CBaseAnimating</code>'s case serves to load the particle effect for fire, since any entity with a model can be ignited). Other precache commands include <code>[[PrecacheParticleSystem]]</code> and <code>[[PrecacheScriptSound]]</code>.
 
For this entity we precache the model we will be using, then call the underlying precache function of our base class (which in <code>CBaseAnimating</code>'s case serves to load the particle effect for fire, since any entity with a model can be ignited). Other precache commands include <code>[[PrecacheParticleSystem]]</code> and <code>[[PrecacheScriptSound]]</code>.
Line 94: Line 102:
 
Valve's <code>[[Spawn()]]</code> function is called whenever a new instance of the entity is created, much like its constructor. In fact it is entirely legal to use <code>Spawn()</code> on its own, but for manageability it is recommended that you employ a constructor too to keep variable initialization separate from other code.
 
Valve's <code>[[Spawn()]]</code> function is called whenever a new instance of the entity is created, much like its constructor. In fact it is entirely legal to use <code>Spawn()</code> on its own, but for manageability it is recommended that you employ a constructor too to keep variable initialization separate from other code.
  
<span style="color:green;">//-----------------------------------------------------------------------------
+
<syntaxhighlight lang="cpp">
// Purpose: Sets up the entity's initial state
+
//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------</span>
+
// Purpose: Sets up the entity's initial state
<span style="color:blue;">void</span> CMyModelEntity::Spawn( <span style="color:blue;">void</span> )
+
//-----------------------------------------------------------------------------
{
+
void CMyModelEntity::Spawn( void )
Precache();
+
{
+
Precache();
SetModel( ENTITY_MODEL );
+
 
SetSolid( SOLID_BBOX );
+
SetModel( ENTITY_MODEL );
UTIL_SetSize( <span style="color:blue;">this</span>, -Vector(20,20,20), Vector(20,20,20) );
+
SetSolid( SOLID_BBOX );
}
+
UTIL_SetSize( this, -Vector(20,20,20), Vector(20,20,20) );
 +
}
 +
</syntaxhighlight>
  
 
We call <code>Precache()</code> first, followed by several <code>CBaseAnimating</code> functions. <code>[[SetModel()]]</code> should be obvious if you think back [[#Defining the model|to our earlier define]], but <code>[[SetSolid()]]</code> needs some explanation. It defines the type of shape that our entity will use to test for collisions with other objects in the world. Using the model itself would be far, far too [[expensive]], so Source offers us a set of compromises:
 
We call <code>Precache()</code> first, followed by several <code>CBaseAnimating</code> functions. <code>[[SetModel()]]</code> should be obvious if you think back [[#Defining the model|to our earlier define]], but <code>[[SetSolid()]]</code> needs some explanation. It defines the type of shape that our entity will use to test for collisions with other objects in the world. Using the model itself would be far, far too [[expensive]], so Source offers us a set of compromises:
Line 131: Line 141:
 
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 <code>CMyModelEntity</code>s in a map without running into any CPU issues!
 
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 <code>CMyModelEntity</code>s in a map without running into any CPU issues!
  
<span style="color:green;">//-----------------------------------------------------------------------------
+
<syntaxhighlight lang="cpp">
// Purpose: Think function to randomly move the entity
+
//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------</span>
+
// Purpose: Think function to randomly move the entity
<span style="color:blue;">void</span> CMyModelEntity::MoveThink( <span style="color:blue;">void</span> )
+
//-----------------------------------------------------------------------------
{
+
void CMyModelEntity::MoveThink( void )
<span style="color:green;">// See if we should change direction again</span>
+
{
<span style="color:blue;">if</span> ( m_flNextChangeTime < gpGlobals->curtime )
+
// See if we should change direction again
{
+
if ( m_flNextChangeTime < gpGlobals->curtime )
<span style="color:green;">// Randomly take a new direction and speed</span>
+
{
Vector vecNewVelocity = RandomVector( -64.0f, 64.0f );
+
// Randomly take a new direction and speed
SetAbsVelocity( vecNewVelocity );
+
Vector vecNewVelocity = RandomVector( -64.0f, 64.0f );
+
SetAbsVelocity( vecNewVelocity );
<span style="color:green;">// Randomly change it again within one to three seconds</span>
+
 
m_flNextChangeTime = gpGlobals->curtime + random->RandomFloat( 1.0f, 3.0f );
+
// Randomly change it again within one to three seconds
}
+
m_flNextChangeTime = gpGlobals->curtime + random->RandomFloat( 1.0f, 3.0f );
+
}
<span style="color:green;">// Snap our facing to where we're heading</span>
+
 
Vector velFacing = GetAbsVelocity();
+
// Snap our facing to where we're heading
QAngle angFacing;
+
Vector velFacing = GetAbsVelocity();
VectorAngles( velFacing, angFacing );
+
QAngle angFacing;
  SetAbsAngles( angFacing );
+
VectorAngles( velFacing, angFacing );
+
SetAbsAngles( angFacing );
<span style="color:green;">// Think at 20Hz</span>
+
 
SetNextThink( gpGlobals->curtime + 0.05f );
+
// Think at 20Hz
}
+
SetNextThink( gpGlobals->curtime + 0.05f );
 +
}
 +
</syntaxhighlight>
  
 
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.
 
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.
Line 176: Line 188:
 
Now we come to our last function. This is an input that will toggle movement on and off.
 
Now we come to our last function. This is an input that will toggle movement on and off.
  
<span style="color:green;">//-----------------------------------------------------------------------------
+
<syntaxhighlight lang="cpp">
// Purpose: Toggle the movement of the entity
+
//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------</span>
+
// Purpose: Toggle the movement of the entity
<span style="color:blue;">void</span> CMyModelEntity::InputToggle( inputdata_t &inputData )
+
//-----------------------------------------------------------------------------
{
+
void CMyModelEntity::InputToggle( inputdata_t &inputData )
<span style="color:green;">// Toggle our active state</span>
+
{
<span style="color:blue;">if</span> ( !m_bActive )
+
// Toggle our active state
{
+
if ( !m_bActive )
<span style="color:green;">// Start thinking</span>
+
{
SetThink( &CMyModelEntity::MoveThink );
+
// Start thinking
+
SetThink( &CMyModelEntity::MoveThink );
SetNextThink( gpGlobals->curtime + 0.05f );
+
 
+
SetNextThink( gpGlobals->curtime + 0.05f );
<span style="color:green;">// Start moving</span>
+
SetMoveType( MOVETYPE_FLY );
+
// Start moving
+
SetMoveType( MOVETYPE_FLY );
<span style="color:green;">// Force MoveThink() to choose a new speed and direction immediately</span>
+
 
m_flNextChangeTime = gpGlobals->curtime;
+
// Force MoveThink() to choose a new speed and direction immediately
+
m_flNextChangeTime = gpGlobals->curtime;
<span style="color:green;">// Update m_bActive to reflect our new state</span>
+
 
m_bActive = <span style="color:blue;">true</span>;
+
// Update m_bActive to reflect our new state
}
+
m_bActive = true;
<span style="color:blue;">else</span>
+
}
{
+
else
<span style="color:green;">// Stop thinking</span>
+
{
SetThink( NULL );
+
// Stop thinking
+
SetThink( NULL );
<span style="color:green;">// Stop moving</span>
+
SetAbsVelocity( vec3_origin );
+
// Stop moving
  SetMoveType( MOVETYPE_NONE );
+
SetAbsVelocity( vec3_origin );
+
SetMoveType( MOVETYPE_NONE );
m_bActive = <span style="color:blue;">false</span>;
+
}
+
m_bActive = false;
}
+
}
 +
}
 +
</syntaxhighlight>
  
 
This is all very straightforward. We use an <code>if</code> statement to check whether or not <code>m_bActive</code> is true. The exclamation mark means "not": "if <code>m_bActive</code> is not true, do this". Later on, we use the <code>else</code> command to specify what we want to do in any other case - which with a boolean value can only be if <code>m_bActive</code> ''is'' true.
 
This is all very straightforward. We use an <code>if</code> statement to check whether or not <code>m_bActive</code> is true. The exclamation mark means "not": "if <code>m_bActive</code> is not true, do this". Later on, we use the <code>else</code> command to specify what we want to do in any other case - which with a boolean value can only be if <code>m_bActive</code> ''is'' true.
Line 221: Line 235:
 
The FGD entry for this entity displays the model in Hammer and allows you to name it and send the input "Toggle".
 
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."
+
<syntaxhighlight>
[
+
@PointClass base(Targetname) studio("models/gibs/airboat_broken_engine.mdl")= my_model_entity :  "Tutorial model entity."
input Toggle(void) : "Toggle movement."
+
[
]
+
input Toggle(void) : "Toggle movement."
 +
]
 +
</syntaxhighlight>
  
 
== The working entity ==
 
== The working entity ==
Line 240: Line 256:
 
; If I spawn it from the console, it will be stuck halfway inside the wall
 
; If I spawn it from the console, it will be stuck halfway inside the wall
 
:You can add this code to your <code>sdk_modelentity.cpp</code>
 
:You can add this code to your <code>sdk_modelentity.cpp</code>
<pre>CON_COMMAND(create_sdk_modelentity, "Creates an instance of the sdk model entity in front of the player.")
+
 
 +
<syntaxhighlight lang="cpp">
 +
CON_COMMAND(create_sdk_modelentity, "Creates an instance of the sdk model entity in front of the player.")
 
{
 
{
 
Vector vecForward;
 
Vector vecForward;
Line 260: Line 278:
 
DispatchSpawn(pEnt);
 
DispatchSpawn(pEnt);
 
}
 
}
}</pre>
+
}
 +
</syntaxhighlight>
 +
 
 
:Then type <code>create_sdk_modelentity</code> in the console to spawn the entity.
 
:Then type <code>create_sdk_modelentity</code> in the console to spawn the entity.
 
:Alternatively, simply place the entity in Hammer.
 
:Alternatively, simply place the entity in Hammer.
Line 267: Line 287:
  
 
In order to add animation we need a model with animations, in this tutorial we will use the scanner.  Change <code>ENTITY_MODEL</code> to <code>"models/combine_scanner.mdl"</code>.  Then add this to <code>Spawn()</code> function.
 
In order to add animation we need a model with animations, in this tutorial we will use the scanner.  Change <code>ENTITY_MODEL</code> to <code>"models/combine_scanner.mdl"</code>.  Then add this to <code>Spawn()</code> function.
<span style="color:green;">// Select the scanner's idle sequence</span>
+
 
SetSequence( LookupSequence("idle") );
+
<syntaxhighlight lang="cpp">
<span style="color:green;">// Set the animation speed to 100%</span>
+
// Select the scanner's idle sequence
SetPlaybackRate( 1.0f );
+
SetSequence( LookupSequence("idle") );
<span style="color:green;">// Tell the client to animate this model</span>
+
// Set the animation speed to 100%
UseClientSideAnimation();
+
SetPlaybackRate( 1.0f );
 +
// Tell the client to animate this model
 +
UseClientSideAnimation();
 +
</syntaxhighlight>
 +
 
 
With any luck you should notice the scanner's "ears" and "face" moving.
 
With any luck you should notice the scanner's "ears" and "face" moving.
  
 
== See also ==
 
== See also ==
 
+
* [[Authoring a Model Entity/Code|Tutorial code in full]]
*[[Authoring a Model Entity/Code|Tutorial code in full]]
+
* [[Your First Entity]]
*[[Your First Entity]]
 
  
 
[[Category:Programming]]
 
[[Category:Programming]]
 
[[Category:Tutorials]]
 
[[Category:Tutorials]]

Revision as of 23:35, 28 June 2011

Русский

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 sdk_modelentity.cpp in your personal Server folder 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();

	CMyModelEntity()
	{
		m_bActive = false;
	}

	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).

Note:Our entity won't begin moving until prompted by InputToggle(). Changing this behaviour so that it starts active (which isn't just as simple as changing m_bActive in the code above!) would be a good project once you finish the tutorial proper.

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/).

Note:We are only creating this define to make finding and changing its value later on easier. It has no effect on how our code works.

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 );

	BaseClass::Precache();
}

For this entity we precache the model we will be using, then call the underlying precache function of our base class (which in CBaseAnimating's case serves to load the particle effect for fire, since any entity with a model can be ignited). Other precache commands include PrecacheParticleSystem and PrecacheScriptSound.

Spawn()

Valve's Spawn() function is called whenever a new instance of the entity is created, much like its constructor. In fact it is entirely legal to use Spawn() on its own, but for manageability it is recommended that you employ a constructor too to keep variable initialization separate from other code.

//-----------------------------------------------------------------------------
// 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) );
}

We call Precache() first, followed by several CBaseAnimating functions. SetModel() should be obvious if you think back to our earlier define, but SetSolid() needs some explanation. It defines the type of shape that our entity will use to test for collisions with other objects in the world. Using the model itself would be far, far too expensive, so Source offers us a set of compromises:

SOLID_NONE
Not solid.
SOLID_BBOX
Uses an axis-aligned bounding box.
SOLID_BSP
Uses the BSP tree to determine solidity (used for brushes).
SOLID_CUSTOM
Entity defines its own functions for testing collisions.
SOLID_VPHYSICS
Uses a model's embedded collision model to test accurate physics collisions.

These are engine-level choices that mod authors cannot change or add to. We are using SOLID_BBOX, which generates a "bounding box" that is sized by the engine to completely enclose our model. The more expensive SOLID_VPHYSICS, which physically simulates collisions based on the model's embedded collision model, doesn't support the low-level movement functions we'll be using for this entity and should be avoided.

We call UTIL_SetSize() to make our bounding box a cube. This is done because, as bizarre as this might at first sound, bounding boxes cannot rotate. You will need vphysics collisions if you want rotation, and as noted above we aren't using them.

Warning: 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 CMyModelEntitys 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 at 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 variables 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 ,like VectorAngles(), 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.

You will have noticed that we are defining new variables here. Like the variables in the class declaration, which are internal to the class, these are internal to this particular function. They are created every time the function is called and destroyed when its execution completes.

Tip:We don't really need the vecNewVelocity. See if you can work out how to pass a value to SetAbsVelocity() without creating any new variables. Remember why we put void in front of all of our functions?

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 moving
		SetMoveType( MOVETYPE_FLY );

		// Force MoveThink() to choose a new speed and direction immediately
		m_flNextChangeTime = gpGlobals->curtime;

		// Update m_bActive to reflect our new state
		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 active 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

my_model_entity in-game.

Place the entity in your map with Hammer. Remember that you need to call the 'toggle' input before it will start moving; you can create another entity (perhaps logic_auto) or use the console command ent_fire my_model_entity toggle to do this.

You'll notice a few things:

The entity doesn't collide with physics objects
The SetAbs* functions interfere with physics calculations. If you want to physically simulate collisions, you'll need to use a different method of movement.
It will dodge around me every time I get in its way
It isn't clear why this happens. Perhaps CBaseAnimating thinks every time its path is blocked?
If I spawn it from the console, it will be stuck halfway inside the wall
You can add this code to your sdk_modelentity.cpp
CON_COMMAND(create_sdk_modelentity, "Creates an instance of the sdk model entity in front of the player.")
{
	Vector vecForward;
	CBasePlayer *pPlayer = UTIL_GetCommandClient();
	if(!pPlayer)
	{
		Warning("Could not determine calling player!\n");
		return;
	}

	AngleVectors( pPlayer->EyeAngles(), &vecForward );
	CBaseEntity *pEnt = CreateEntityByName( "my_model_entity" );
	if ( pEnt )
	{
		Vector vecOrigin = pPlayer->GetAbsOrigin() + vecForward * 256 + Vector(0,0,64);
		QAngle vecAngles(0, pPlayer->GetAbsAngles().y - 90, 0);
		pEnt->SetAbsOrigin(vecOrigin);
		pEnt->SetAbsAngles(vecAngles);
		DispatchSpawn(pEnt);
	}
}
Then type create_sdk_modelentity in the console to spawn the entity.
Alternatively, simply place the entity in Hammer.

Animating this entity

In order to add animation we need a model with animations, in this tutorial we will use the scanner. Change ENTITY_MODEL to "models/combine_scanner.mdl". Then add this to Spawn() function.

// Select the scanner's idle sequence
SetSequence( LookupSequence("idle") );
// Set the animation speed to 100%
SetPlaybackRate( 1.0f );
// Tell the client to animate this model
UseClientSideAnimation();

With any luck you should notice the scanner's "ears" and "face" moving.

See also