Authoring a Model Entity:zh-cn

From Valve Developer Community
Revision as of 01:16, 17 March 2019 by Kalenclark (talk | contribs)

Jump to: navigation, search
English Русский

本教程假设你已经完成并理解了制作一个逻辑实体这篇文章。

在本教程中,我们将会创建一个可以移动,与其他物体碰撞,并且可以看得见的实体。我们将会使该实体在游戏世界里四处移动。

在你的Server目录创建sdk_modelentity.cpp文件然后我们就可以开始编写代码了。

包含头文件与声明

你现在应该能够理解下面的代码了:

#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;
};

注意观察我们的类现在是如何继承于CBaseAnimating以及我们是如何创建一些新的函数的。私有成员包括一个布尔型变量以及一个浮点型变量(小数)。

注意:我们的实体刚开始并不会移动,只有InputToggle()被触发时它才移动。当你跟着教程正确编写好代码之后,要想改变这一默认设定(使它一开始就四处移动),不能只是简单地修改上面代码中的m_bActive,但实现它一开始就四处移动的效果却是一个不错的练习。

实体名称与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()

相较于我们的逻辑实体,这里最大的改变就是DEFINE_THINKFUNCThink函数是Source引擎里的特殊情况,所以我们需要区分它们。

定义模型

// Name of our entity's model
#define	ENTITY_MODEL	"models/gibs/airboat_broken_engine.mdl"

上面的代码用宏来指定(硬编码)了我们的实体的模型。这是一条静态信息而不是一个变量:当代码被编译过后它就不能改变了。访问.mdl文件的路径是相对于游戏目录的 (即 hl2/)。

注意:我们是为了在后面更方便地查找或修改所要使用的模型文件才创建这个宏定义的,它对于我们的代码如何工作没有任何影响。

Precache()

现在我们看到我们的第一个函数。生成一个实体时,应该调用Precache(),并且确保列在其中的所有东西在玩家出生(即看到游戏世界并且获得控制)之前都被缓存好。查阅缓存游戏资源文件来获取更多的信息。

因为“在玩家出生之后”有其他种类的"异步"加载会发生,所以Precaching有一个特殊的名字。

//-----------------------------------------------------------------------------
// Purpose: Precache assets used by the entity
//-----------------------------------------------------------------------------
void CMyModelEntity::Precache( void )
{
	PrecacheModel( ENTITY_MODEL );

	BaseClass::Precache();
}

对于这个实体,我们在这里缓存它将要用到的模型,然后调用它的基类的缓存函数(对于CBaseAnimating这种情况,该函数用于加载火焰的粒子效果,因为有模型的实体都可以被点燃)。其他缓存命令包括PrecacheParticleSystem还有PrecacheScriptSound.

Spawn()

Valve的Spawn()函数是在一个实体的实例被创建时被调用的,很像它的构造函数。事实上,自己使用Spawn()是合法的,但是为了便于管理,我们还是推荐你同时使用一个构造函数来将变量初始化的过程从代码中分离出来。

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

我们首先调用Precache(),然后紧接着是一些CBaseAnimating函数。如果你能回想起(回到之前的定义)SetModel()的功能显而易见。,但是我们需要解释一下SetSolid()。它定义了用于测试碰撞的形状类型。使用模型自身的形状来测试碰撞所付出的性能代价太大太大了,所以Source引擎提供了一些折中方案:

SOLID_NONE
非固体。
SOLID_BBOX
使用轴对齐碰撞盒。
SOLID_BSP
使用BSP树来决定固性(用于固体)。
SOLID_CUSTOM
实体定义自己的碰撞测试函数。
SOLID_VPHYSICS
使用一个模型的内含的[[collision model|碰撞模型]来测试出精确的物理碰撞。

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