Rotating Pickups: Difference between revisions

From Valve Developer Community
Jump to navigation Jump to search
No edit summary
m (→‎top: clean up, added orphan tag)
 
(15 intermediate revisions by 4 users not shown)
Line 1: Line 1:
This tutorial will show you how to make a rotating pickup, like the ones in TF2. It can easily be modified to use other models, rotate at other speeds and angles.
{{Orphan|date=January 2024}}


<pre>
{{toc-right}}
//================== Coder: Islandstone Operation Nexus =======================//
//
// Purpose: Make a healthkit that rotates
//
//=============================================================================//


#include "cbase.h" // The base for all entities
In this tutorial we'll create a '''rotating health pickup''' entity, like the ones in TF2. When a player walks over it they will gain some health and the item will disappear until a respawn timer completes.
#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!!!
<div style="font-size:120%;">'''[[Rotating Pickups/Code|Get the code here.]]''' You should be familiar with [[Your First Entity|creating entities]], so only the interesting parts will be picked out.</div>
#include "tier0/memdbgon.h"


// This defines the size of a box around our picku
== Functionality ==
#define ITEM_PICKUP_BOX_BLOAT          24


// A full rotation divided by the number of seconds it takes to rotate
; Server
#define ITEM_ROTATION_RATE            ( 360.0f / 9.0f )
: Initialise the entity as a [[trigger]]
: Ensure that the item floats above any walkable surfaces
: Create a [[decal]] to mark the item's position when it is invisible
: Handle touches from players (give health, play a respawn sound)
; Client
: Rotate the model
: Handle model visibility


// Define what will be the default health inside
== Server ==
#define        DEFAULT_HEALTH_TO_GIVE        25


// Define what will be the default respawn time
=== Spawn() ===
#define        DEFAULT_RESPAWN_TIME          20


* Note how some of <code>[[CItem]]</code>'s changes have to be undone (it's not designed with impossible floating objects in mind). Source is a big and mature engine, and it's always worth checking what your base classes are doing.
* We are setting <code>MdlTop</code> before fiddling about with the bounding box. This will be used when repositioning the entity; if we just used the origin, then if/when a larger model is used it might reach so high that it obscures the player's view.


//-----------------------------------------------------------------------------
=== Activate() ===
// Small rotating health kit. Heals the player when picked up.
//-----------------------------------------------------------------------------
class CRotatingPickup : public CItem
{
public:
DECLARE_CLASS( CRotatingPickup, CItem );
DECLARE_DATADESC();


CRotatingPickup();
* Here we trace down to our desired height, adding to the entity's Z position if we come up short. This is done in <code>Activate()</code> instead of <code>Spawn()</code> in case we spawned before the entity beneath us.
* This is also where we fire a decal onto the ground. This might look bad in some cases, so a flag to bypass the call is provided.


void Spawn( void );
=== Precache() ===
void Precache( void );


bool MyTouch( CBasePlayer *pPlayer );
* We need to get the return value from <code>UTIL_PrecacheDecal()</code>, but <code>Precace()</code> is called statically (i.e. directly, not through any instance of the class). Storing the value in a global variable is the only sensible solution.
void FallThink( void ) { return; } // Override the function that makes items fall to the ground
void RotateThink( void );


CBaseEntity* Respawn( void );
== Client ==
void Materialize( void );


protected:
=== ClientThink() ===


int m_iHealthToGive;
* <code>C_RotatingPickup::ClientRotAng</code> is used to rotate the model, not <code>CBaseEntity</code>'s built-in variable. This is because even though the latter has been excluded from this entity's [[datatable]], it is still zeroed when the entity materialises (I suspect that <code>CItem</code> is resetting the entity's location through some sort of back door). Not calling <code>GetAbsAngles()</code> also provides a small performance boost.
int m_iRespawnTime;
Vector RespawnPosition;


private:
=== PostDataUpdate() ===


void UpdateSpawnPosition( Vector originalSpawnPosition );
* If you have looked into [[Networking Entities#Send & Receive Proxies|receive proxies]], you may be wondering why we are testing against a cached value when we could simply hook into the receipt of an updated one. Two reasons:
*# You can't have proxies on <code>RecvPropBool()</code>.
*# Proxies are static and should never, ''ever'' be used to run entity logic. Receive proxies are called not only when receiving entity updates, but also when validating [[Prediction|predicted]] values. It's quite possible for a proxy to be re-used by another entity too.


};
[[Category:Programming]]
 
[[Category:Free source code]]
LINK_ENTITY_TO_CLASS( item_rotating, CRotatingPickup );
[[Category:Tutorials]]
 
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()
 
//-----------------------------------------------------------------------------
// 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;
}
 
//-----------------------------------------------------------------------------
// 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 );
}
 
void CRotatingPickup::UpdateSpawnPosition( Vector originalPosition )
{
// Create local variables
trace_t tr; // The trace
Vector end, dir, final; // The vectors
QAngle down; // Th angle
 
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
}
 
//-----------------------------------------------------------------------------
// 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
}
 
 
//-----------------------------------------------------------------------------
// Purpose: Give the player health and play 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;
}
 
//-----------------------------------------------------------------------------
// 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;
}
 
//-----------------------------------------------------------------------------
// 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
}
 
//-----------------------------------------------------------------------------
// 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
}
</pre>
 
{{todo|Make a tutorial/article that explains the code}}

Latest revision as of 22:52, 21 January 2024

In this tutorial we'll create a rotating health pickup entity, like the ones in TF2. When a player walks over it they will gain some health and the item will disappear until a respawn timer completes.

Get the code here. You should be familiar with creating entities, so only the interesting parts will be picked out.

Functionality

Server
Initialise the entity as a trigger
Ensure that the item floats above any walkable surfaces
Create a decal to mark the item's position when it is invisible
Handle touches from players (give health, play a respawn sound)
Client
Rotate the model
Handle model visibility

Server

Spawn()

  • Note how some of CItem's changes have to be undone (it's not designed with impossible floating objects in mind). Source is a big and mature engine, and it's always worth checking what your base classes are doing.
  • We are setting MdlTop before fiddling about with the bounding box. This will be used when repositioning the entity; if we just used the origin, then if/when a larger model is used it might reach so high that it obscures the player's view.

Activate()

  • Here we trace down to our desired height, adding to the entity's Z position if we come up short. This is done in Activate() instead of Spawn() in case we spawned before the entity beneath us.
  • This is also where we fire a decal onto the ground. This might look bad in some cases, so a flag to bypass the call is provided.

Precache()

  • We need to get the return value from UTIL_PrecacheDecal(), but Precace() is called statically (i.e. directly, not through any instance of the class). Storing the value in a global variable is the only sensible solution.

Client

ClientThink()

  • C_RotatingPickup::ClientRotAng is used to rotate the model, not CBaseEntity's built-in variable. This is because even though the latter has been excluded from this entity's datatable, it is still zeroed when the entity materialises (I suspect that CItem is resetting the entity's location through some sort of back door). Not calling GetAbsAngles() also provides a small performance boost.

PostDataUpdate()

  • If you have looked into receive proxies, you may be wondering why we are testing against a cached value when we could simply hook into the receipt of an updated one. Two reasons:
    1. You can't have proxies on RecvPropBool().
    2. Proxies are static and should never, ever be used to run entity logic. Receive proxies are called not only when receiving entity updates, but also when validating predicted values. It's quite possible for a proxy to be re-used by another entity too.