Rotating Pickups: Difference between revisions
m (→Respawn()) |
TomEdwards (talk | contribs) (cleaned) |
||
Line 1: | Line 1: | ||
{{note|This tutorial and its related code is only meant to give the reader a view on what angles, vectors and tracelines can be used for. If you want to implement something like this in a game, '''do the effect code client-side'''}} | {{note|This tutorial and its related code is only meant to give the reader a view on what angles, vectors and tracelines can be used for. If you want to implement something like this in a game, '''do the effect code client-side'''}} | ||
In this tutorial we'll create an entity that will rotate at a fixed position above ground, and will act as a health pickup, like the ones in TF2. When picked up it will disappear, before it respawns after a certain amount of seconds. This code was made for and tested with OB HL2MP sdk. | In this tutorial we'll create an entity that will rotate at a fixed position above ground, and will act as a health pickup, like the ones in TF2. When picked up it will disappear, before it respawns after a certain amount of seconds. This code was made for and tested with OB HL2MP sdk. | ||
Line 7: | Line 6: | ||
*Completed and understood all the beginner tutorials | *Completed and understood all the beginner tutorials | ||
*Know how to make an FGD entry | *Know how to make an FGD entry | ||
== Includes and definitions == | == Includes and definitions == | ||
< | <source lang=cpp> | ||
#include "cbase.h" // The base for all entities | #include "cbase.h" // The base for all entities | ||
#include "player.h" // The player itself | #include "player.h" // The player itself | ||
Line 33: | Line 28: | ||
// Define what will be the default respawn time | // Define what will be the default respawn time | ||
#define DEFAULT_RESPAWN_TIME 20 | #define DEFAULT_RESPAWN_TIME 20 | ||
</ | </source> | ||
First off create a new file. What you name it is up to you. Begin by putting the above code in the file. The includes should be fairly easy to understand why. The player.h is included because we'll access the player code to set the players health. | First off create a new file. What you name it is up to you. Begin by putting the above code in the file. The includes should be fairly easy to understand why. The player.h is included because we'll access the player code to set the players health. | ||
Line 43: | Line 38: | ||
== Class declaration and datadesc == | == Class declaration and datadesc == | ||
< | <source lang=cpp> | ||
//----------------------------------------------------------------------------- | //----------------------------------------------------------------------------- | ||
// Small rotating health kit. Heals the player when picked up. | // Small rotating health kit. Heals the player when picked up. | ||
Line 89: | Line 84: | ||
END_DATADESC() | END_DATADESC() | ||
</ | </source> | ||
Most of these functions are just overrides of fuctions defined in our base class, CItem. The interesting one there is FallThink(), which just contains a simple return. The reason for this is because the FallThink() functions causes the item to fall to the ground before it activates. Therefore, it's overridden with a simple return, so if it ever gets called within our class, it won't do any harm. The datadesc should not be new to you if you've finished the beginner tutorials. | Most of these functions are just overrides of fuctions defined in our base class, CItem. The interesting one there is FallThink(), which just contains a simple return. The reason for this is because the FallThink() functions causes the item to fall to the ground before it activates. Therefore, it's overridden with a simple return, so if it ever gets called within our class, it won't do any harm. The datadesc should not be new to you if you've finished the beginner tutorials. | ||
Line 95: | Line 90: | ||
== Constructor == | == Constructor == | ||
< | <source lang=cpp> | ||
//----------------------------------------------------------------------------- | //----------------------------------------------------------------------------- | ||
// Purpose: Initialize member variables | // Purpose: Initialize member variables | ||
Line 109: | Line 104: | ||
m_iRespawnTime = DEFAULT_RESPAWN_TIME; | m_iRespawnTime = DEFAULT_RESPAWN_TIME; | ||
} | } | ||
</ | </source> | ||
Here we have the variables we need to initialize, and it's pretty straightforward code. Note that m_bShouldFall is a member of the base class, and it's set as an extra precaution. | Here we have the variables we need to initialize, and it's pretty straightforward code. Note that m_bShouldFall is a member of the base class, and it's set as an extra precaution. | ||
== Spawn() and Precache() == | == Spawn() and Precache() == | ||
< | <source lang=cpp> | ||
//----------------------------------------------------------------------------- | //----------------------------------------------------------------------------- | ||
// Purpose: | // Purpose: | ||
Line 157: | Line 151: | ||
PrecacheScriptSound( "HealthKit.Touch" ); // scripts/game_sounds_items.txt | PrecacheScriptSound( "HealthKit.Touch" ); // scripts/game_sounds_items.txt | ||
} | } | ||
</ | </source> | ||
You should feel familiar here. We set the movetypes, solid states and collision data accordingly. CollisionProp is what creates the trigger around the model, and SetTouch() is the function that will get called every time the trigger is touched. ItemTouch() isn't specified within our class and is again a member of the base class. | You should feel familiar here. We set the movetypes, solid states and collision data accordingly. CollisionProp is what creates the trigger around the model, and SetTouch() is the function that will get called every time the trigger is touched. ItemTouch() isn't specified within our class and is again a member of the base class. | ||
Line 166: | Line 160: | ||
QAngles might be something new to you. QAngles are basically what's used to define angles in Source. As you can see, we start by making the angle to be exactly as the level designer placed it. Then we override the X-axis to tilt it. You might have to change what axis and how much you tilt it if you use another model. Next we set the model to be the HL2 health kit, and tell the entity to start thinking. The precache is just two lines, one for the sound the item uses and another one for the model. Let's continue to the UpdateSpawnPosition() function. | QAngles might be something new to you. QAngles are basically what's used to define angles in Source. As you can see, we start by making the angle to be exactly as the level designer placed it. Then we override the X-axis to tilt it. You might have to change what axis and how much you tilt it if you use another model. Next we set the model to be the HL2 health kit, and tell the entity to start thinking. The precache is just two lines, one for the sound the item uses and another one for the model. Let's continue to the UpdateSpawnPosition() function. | ||
== UpdateSpawnPosition() == | == UpdateSpawnPosition() == | ||
< | <source lang=cpp> | ||
void CRotatingPickup::UpdateSpawnPosition( Vector originalPosition ) | void CRotatingPickup::UpdateSpawnPosition( Vector originalPosition ) | ||
{ | { | ||
Line 194: | Line 187: | ||
RespawnPosition = final; // Store our position for easy access later | RespawnPosition = final; // Store our position for easy access later | ||
} | } | ||
</ | </source> | ||
In this function we use two important things from the source engine to move the item to the correct position above the ground. If you haven't done so already, you should read up on how TraceLines work. | In this function we use two important things from the source engine to move the item to the correct position above the ground. If you haven't done so already, you should read up on how TraceLines work. | ||
Line 201: | Line 194: | ||
By multiplying the sum of the two vectors we can define the length of it. The we create an actual trace line. We don't want our model to spawn in the ground, so we add 50 units in height from the ground position. The last thing we do in the function is to set our new position, and update the position it will respawn at. | By multiplying the sum of the two vectors we can define the length of it. The we create an actual trace line. We don't want our model to spawn in the ground, so we add 50 units in height from the ground position. The last thing we do in the function is to set our new position, and update the position it will respawn at. | ||
== MyTouch() == | == MyTouch() == | ||
< | <source lang=cpp> | ||
//----------------------------------------------------------------------------- | //----------------------------------------------------------------------------- | ||
// Purpose: Give the player health and plays a sound | // Purpose: Give the player health and plays a sound | ||
Line 239: | Line 231: | ||
return false; | return false; | ||
} | } | ||
</ | </source> | ||
This is where the player will get the health he deserves. We want to make sure we have a valid player and that he doesn't have max hp when he picks it up. If he doesn't, we give him the specified health. The next few lines of code is a usermessage. What this does in this case is to inform the player that he picked up something. This is usually done via a hud element. Since there's no hud element defined for it, you won't see anything before you actually add one. The next lines takes care of the sound output that comes every time a player picks up the health kit, and finally it calls the Respawn() function to make sure our health kit respawns. | This is where the player will get the health he deserves. We want to make sure we have a valid player and that he doesn't have max hp when he picks it up. If he doesn't, we give him the specified health. The next few lines of code is a usermessage. What this does in this case is to inform the player that he picked up something. This is usually done via a hud element. Since there's no hud element defined for it, you won't see anything before you actually add one. The next lines takes care of the sound output that comes every time a player picks up the health kit, and finally it calls the Respawn() function to make sure our health kit respawns. | ||
Line 245: | Line 237: | ||
== Respawn() == | == Respawn() == | ||
< | <source lang=cpp> | ||
//----------------------------------------------------------------------------- | //----------------------------------------------------------------------------- | ||
// Purpose: Initiate the respawning process | // Purpose: Initiate the respawning process | ||
Line 275: | Line 267: | ||
return this; | return this; | ||
} | } | ||
</ | </source> | ||
This function takes care of the respawning process. It should be noted that our entity is never actually removed, but it's just set to an invisible state. The function acts as a secondary spawn function, where the position and angles are reset. | This function takes care of the respawning process. It should be noted that our entity is never actually removed, but it's just set to an invisible state. The function acts as a secondary spawn function, where the position and angles are reset. | ||
Line 281: | Line 273: | ||
== Materialize() == | == Materialize() == | ||
< | <source lang=cpp> | ||
//----------------------------------------------------------------------------- | //----------------------------------------------------------------------------- | ||
// Purpose: Finalize the respawning process | // Purpose: Finalize the respawning process | ||
Line 300: | Line 292: | ||
SetNextThink( gpGlobals->curtime + 0.01f ); // Think in 0.01 sec | SetNextThink( gpGlobals->curtime + 0.01f ); // Think in 0.01 sec | ||
} | } | ||
</source> | |||
</ | |||
In reality, it's the Materialize() function that does the actual "respawning". Although what it really does is just remove the EF_NODRAW flag we set earlier in Respawn(). It also resets our touch function and emits a sound, before it set the next think function, that will take care of our rotation. | In reality, it's the Materialize() function that does the actual "respawning". Although what it really does is just remove the EF_NODRAW flag we set earlier in Respawn(). It also resets our touch function and emits a sound, before it set the next think function, that will take care of our rotation. | ||
== RotateThink() == | == RotateThink() == | ||
< | <source lang=cpp> | ||
//----------------------------------------------------------------------------- | //----------------------------------------------------------------------------- | ||
// Purpose: Make our model rotate | // Purpose: Make our model rotate | ||
Line 329: | Line 318: | ||
SetNextThink( gpGlobals->curtime + 0.01f ); // Think again in 0.01 sec | SetNextThink( gpGlobals->curtime + 0.01f ); // Think again in 0.01 sec | ||
} | } | ||
</ | </source> | ||
We only have one more function left to define, and that is the think function that will rotate the model. To make sure the model can rotate at correct speed independent of the fps, we use something called Delta Time, which is basically the current time - the last time the entity was thinking. Which means that if this think was delayed for some reason, it will still compensate for it. To avoid the angle variables to get insanely large, we subtract 360 every time it's above 360. | We only have one more function left to define, and that is the think function that will rotate the model. To make sure the model can rotate at correct speed independent of the fps, we use something called Delta Time, which is basically the current time - the last time the entity was thinking. Which means that if this think was delayed for some reason, it will still compensate for it. To avoid the angle variables to get insanely large, we subtract 360 every time it's above 360. | ||
== When you're done == | == When you're done == |
Revision as of 04:01, 30 March 2011

In this tutorial we'll create an entity that will rotate at a fixed position above ground, and will act as a health pickup, like the ones in TF2. When picked up it will disappear, before it respawns after a certain amount of seconds. This code was made for and tested with OB HL2MP sdk.
Before you start this tutorial you should have:
- Completed and understood all the beginner tutorials
- Know how to make an FGD entry
Includes and definitions
#include "cbase.h" // The base for all entities
#include "player.h" // The player itself
#include "items.h" // Where we derive our class from
#include "engine/IEngineSound.h" // This takes care of the sounds
// memdbgon must be the last include file in a .cpp file!!!
#include "tier0/memdbgon.h"
// This defines the size of a box around our pickup
#define ITEM_PICKUP_BOX_BLOAT 24
// A full rotation divided by the number of seconds it takes to rotate
#define ITEM_ROTATION_RATE ( 360.0f / 9.0f )
// Define what will be the default health inside
#define DEFAULT_HEALTH_TO_GIVE 25
// Define what will be the default respawn time
#define DEFAULT_RESPAWN_TIME 20
First off create a new file. What you name it is up to you. Begin by putting the above code in the file. The includes should be fairly easy to understand why. The player.h is included because we'll access the player code to set the players health.
The ITEM_PICKUP_BOX_BLOAT is the most confusing one here. It's really just a number that defines the size of a box shape around the model. If you change the model, you might need to raise/lower this value to prevent the player from having to walk into the model before being able to pick it up.
ITEM_ROTATION_RATE controls how fast our model should rotate. It's written in it's mathematical manner to show the logic behind it. You could really just put 40 in there and it would still work. However, as the calculation is done by the preprocessor, there isn't much overhead to writing it this way. The parentheses are there as a safety measure to make sure the preprocessor reads it correctly. The last two are just fallback values, i.e. if a mapmaker doesn't specify any other values, those will be used instead.
Class declaration and datadesc
//-----------------------------------------------------------------------------
// Small rotating health kit. Heals the player when picked up.
//-----------------------------------------------------------------------------
class CRotatingPickup : public CItem
{
public:
DECLARE_CLASS( CRotatingPickup, CItem );
DECLARE_DATADESC();
CRotatingPickup();
void Spawn( void );
void Precache( void );
bool MyTouch( CBasePlayer *pPlayer );
void FallThink( void ) { return; } // Override the function that makes items fall to the ground
void RotateThink( void );
CBaseEntity* Respawn( void );
void Materialize( void );
protected:
int m_iHealthToGive;
int m_iRespawnTime;
Vector RespawnPosition;
private:
void UpdateSpawnPosition( Vector originalSpawnPosition );
};
LINK_ENTITY_TO_CLASS( item_rotating, CRotatingPickup );
PRECACHE_REGISTER( item_rotating );
BEGIN_DATADESC( CRotatingPickup )
DEFINE_KEYFIELD( m_iHealthToGive, FIELD_INTEGER, "health"),
DEFINE_KEYFIELD( m_iRespawnTime, FIELD_INTEGER, "respawntime"),
DEFINE_THINKFUNC( RotateThink ),
END_DATADESC()
Most of these functions are just overrides of fuctions defined in our base class, CItem. The interesting one there is FallThink(), which just contains a simple return. The reason for this is because the FallThink() functions causes the item to fall to the ground before it activates. Therefore, it's overridden with a simple return, so if it ever gets called within our class, it won't do any harm. The datadesc should not be new to you if you've finished the beginner tutorials.
Constructor
//-----------------------------------------------------------------------------
// Purpose: Initialize member variables
//-----------------------------------------------------------------------------
CRotatingPickup::CRotatingPickup()
{
m_bShouldFall = false;
if ( m_iHealthToGive <= 0 )
m_iHealthToGive = DEFAULT_HEALTH_TO_GIVE;
if ( m_iRespawnTime <= 0 )
m_iRespawnTime = DEFAULT_RESPAWN_TIME;
}
Here we have the variables we need to initialize, and it's pretty straightforward code. Note that m_bShouldFall is a member of the base class, and it's set as an extra precaution.
Spawn() and Precache()
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
void CRotatingPickup::Spawn( void )
{
BaseClass::Spawn(); //Spawn the baseclass
Precache(); // Make sure the assets are loaded
SetMoveType( MOVETYPE_NONE ); // It will only rotate, not move
SetSolid( SOLID_BBOX ); // It is solid
SetCollisionGroup( COLLISION_GROUP_WEAPON ); // And it can collide with stuff
CollisionProp()->UseTriggerBounds( true, ITEM_PICKUP_BOX_BLOAT ); // Create a collision trigger around the object
SetTouch(&CRotatingPickup::ItemTouch); // ItemTouch is a function in our base class that takes care of touches
UpdateSpawnPosition( GetAbsOrigin() ); // We update our position relative to the ground
// Set the x angle, since it will never change
QAngle angle = GetAbsAngles();
angle.x = 45;
SetAbsAngles( angle );
m_takedamage = DAMAGE_EVENTS_ONLY;
SetModel( "models/items/healthkit.mdl" ); // Set the model we'll use
// Start thinking in 0.01 seconds
SetThink( &CRotatingPickup::RotateThink );
SetNextThink( gpGlobals->curtime + 0.01f );
}
//-----------------------------------------------------------------------------
// Purpose: Make sure the engine loads the sounds and models before they are used
//-----------------------------------------------------------------------------
void CRotatingPickup::Precache( void )
{
PrecacheModel( "models/items/healthkit.mdl" ); // Change this to get another model
PrecacheScriptSound( "HealthKit.Touch" ); // scripts/game_sounds_items.txt
}
You should feel familiar here. We set the movetypes, solid states and collision data accordingly. CollisionProp is what creates the trigger around the model, and SetTouch() is the function that will get called every time the trigger is touched. ItemTouch() isn't specified within our class and is again a member of the base class.
Why are we using that and not our own touch function? The difference between ItemTouch and MyTouch is that ItemTouch is fired every time it gets touched by anything. ItemTouch calls MyTouch whenever there's a player picking up the item. If you have a look at CItem you'll see that MyTouch does nothing. Therefore we override it and place our own code in there.
The next part is where we update both position and angle. We'll define the UpdateSpawnPosition() later. As for now, you can see that it takes the entity's current origin as the input. This function will make sure that the healthkit is always at the same level above ground, no matter how high or low the level designer place it.
QAngles might be something new to you. QAngles are basically what's used to define angles in Source. As you can see, we start by making the angle to be exactly as the level designer placed it. Then we override the X-axis to tilt it. You might have to change what axis and how much you tilt it if you use another model. Next we set the model to be the HL2 health kit, and tell the entity to start thinking. The precache is just two lines, one for the sound the item uses and another one for the model. Let's continue to the UpdateSpawnPosition() function.
UpdateSpawnPosition()
void CRotatingPickup::UpdateSpawnPosition( Vector originalPosition )
{
// Create local variables
trace_t tr; // The trace
Vector end, dir, final; // The vectors
QAngle down; // The angles
down.y = -90; //Make angle point down
AngleVectors( down, &dir); //Make the vector point to the angle
end = originalPosition + dir * MAX_TRACE_LENGTH; // Get the end point
// Trace a line down to the ground
UTIL_TraceLine( originalPosition, end, MASK_SOLID, NULL, COLLISION_GROUP_NONE, &tr );
final = tr.endpos; // final is now the position of the ground right beneath our entity
final.z += 50; // Add 50 units in height
SetAbsOrigin( final ); // Update our position to be 50 units above the ground
RespawnPosition = final; // Store our position for easy access later
}
In this function we use two important things from the source engine to move the item to the correct position above the ground. If you haven't done so already, you should read up on how TraceLines work.
After defining our local variables, lets get to the real business. For our traceline to get the position below, we have to make an angle that points down, and then transfer that direction to a vector. The vector math used to get the end point is somewhat advanced, and I'll not go in depth of that topic here. Search for "Vector addition" on google if you want to learn more about it.
By multiplying the sum of the two vectors we can define the length of it. The we create an actual trace line. We don't want our model to spawn in the ground, so we add 50 units in height from the ground position. The last thing we do in the function is to set our new position, and update the position it will respawn at.
MyTouch()
//-----------------------------------------------------------------------------
// Purpose: Give the player health and plays a sound
// Input : *pPlayer -
// Output :
//-----------------------------------------------------------------------------
bool CRotatingPickup::MyTouch( CBasePlayer *pPlayer )
{
//Check the pointer and check if the player needs more health
if ( pPlayer && pPlayer->GetHealth() < pPlayer->GetMaxHealth() )
{
pPlayer->TakeHealth( m_iHealthToGive, DMG_GENERIC );
// This code is related to the hud
CSingleUserRecipientFilter user( pPlayer );
user.MakeReliable();
UserMessageBegin( user, "ItemPickup" );
WRITE_STRING( GetClassname() );
MessageEnd();
// Output the sound sound
CPASAttenuationFilter filter( pPlayer, "HealthKit.Touch" );
EmitSound( filter, pPlayer->entindex(), "HealthKit.Touch" );
//Msg("A player picked up something!\n" ); //Uncomment this line to get a note in the console when picked up
Respawn(); // Respawn our pickup
return true;
}
return false;
}
This is where the player will get the health he deserves. We want to make sure we have a valid player and that he doesn't have max hp when he picks it up. If he doesn't, we give him the specified health. The next few lines of code is a usermessage. What this does in this case is to inform the player that he picked up something. This is usually done via a hud element. Since there's no hud element defined for it, you won't see anything before you actually add one. The next lines takes care of the sound output that comes every time a player picks up the health kit, and finally it calls the Respawn() function to make sure our health kit respawns.
Respawn()
//-----------------------------------------------------------------------------
// Purpose: Initiate the respawning process
//-----------------------------------------------------------------------------
CBaseEntity* CRotatingPickup::Respawn( void )
{
// It can't be touched and it can't be seen
SetTouch( NULL );
AddEffects( EF_NODRAW );
//Reset the movetypes
SetMoveType( MOVETYPE_NONE );
SetSolid( SOLID_BBOX );
SetCollisionGroup( COLLISION_GROUP_WEAPON );
UTIL_SetOrigin( this, RespawnPosition ); //Get our respawn position from earlier
// Reset the angles
QAngle angle = GetAbsAngles();
angle.x = 45;
SetAbsAngles( angle );
RemoveAllDecals(); //Remove any decals
//Start thinking when the pickup should appear again
SetThink ( &CRotatingPickup::Materialize );
SetNextThink( gpGlobals->curtime + m_iRespawnTime );
return this;
}
This function takes care of the respawning process. It should be noted that our entity is never actually removed, but it's just set to an invisible state. The function acts as a secondary spawn function, where the position and angles are reset.
Materialize()
//-----------------------------------------------------------------------------
// Purpose: Finalize the respawning process
//-----------------------------------------------------------------------------
void CRotatingPickup::Materialize( void )
{
if ( IsEffectActive( EF_NODRAW ) )
{
//Changing from invisible state to visible.
RemoveEffects( EF_NODRAW );
}
EmitSound( "AlyxEmp.Charge" ); //Emit a sound
SetTouch( &CRotatingPickup::ItemTouch ); //Reset our think functions
SetThink( &CRotatingPickup::RotateThink ); // Start rotating again
SetNextThink( gpGlobals->curtime + 0.01f ); // Think in 0.01 sec
}
In reality, it's the Materialize() function that does the actual "respawning". Although what it really does is just remove the EF_NODRAW flag we set earlier in Respawn(). It also resets our touch function and emits a sound, before it set the next think function, that will take care of our rotation.
RotateThink()
//-----------------------------------------------------------------------------
// Purpose: Make our model rotate
//-----------------------------------------------------------------------------
void CRotatingPickup::RotateThink( void )
{
// This makes sure the model rotates independent of the fps
float dt = gpGlobals->curtime - GetLastThink();
QAngle angles = GetAbsAngles(); //Get the current angles
// Set the angles according to the rotation rate and fps
angles.y += ( ITEM_ROTATION_RATE * dt );
if ( angles.y >= 360 ) // If the rotation is more than 360,
angles.y -= 360; // subtract 360 to avoid large variables
SetAbsAngles( angles ); // Set the angles now
SetNextThink( gpGlobals->curtime + 0.01f ); // Think again in 0.01 sec
}
We only have one more function left to define, and that is the think function that will rotate the model. To make sure the model can rotate at correct speed independent of the fps, we use something called Delta Time, which is basically the current time - the last time the entity was thinking. Which means that if this think was delayed for some reason, it will still compensate for it. To avoid the angle variables to get insanely large, we subtract 360 every time it's above 360.
When you're done
After completing this tutorial, you can expand this code to do more. Here's a list of things you can do:
- Create an entry in your FGD file
- Make it rotate at another speed
- Reverse the rotation direction
- Modify the height above ground
- Improve the RotateThink() (It executes even though the model is invisible. What can you do about it?)
- Change the sounds
- Change the model
- Create a sprite that surrounds the model (Advanced)
- Make it bounce up and down along with the rotation (Advanced)
- Make it give you a weapon instead of health (Advanced)