Rotating Pickups: Difference between revisions
Islandstone (talk | contribs) No edit summary |
Islandstone (talk | contribs) m (Added note on the top) |
||
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'''}} | |||
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. | ||
Revision as of 09:50, 5 July 2009

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)