targetname
, so other entities could trigger it, target
, so it can find an entity to trigger, and delay
, to determine how often this entity will trigger its targets. The entity will start inactive (off), and will be toggled by other entities. trigger_auto
or a similar entity.trigger_relay
. We'll need to know this when writing the FGD.#include "extdll.h"
#include "util.h"
#include "cbase.h"
class CTargetTimer : public CBaseEntity
{
public:
void Spawn() override;
void KeyValue( KeyValueData* pkvd ) override;
void Use( CBaseEntity* pActivator, CBaseEntity* pCaller, USE_TYPE useType, float value ) override;
void EXPORT TimerThink();
bool IsEnabled() const;
private:
float m_flTimerDelay = 1.0f; // 1s of delay by default
};
LINK_ENTITY_TO_CLASS( target_timer, CTargetTimer );
For now, Spawn
won't do much, only make sure that the entity starts off:
void CTargetTimer::Spawn()
{
// Entity starts off by default
SetThink( nullptr );
}
In KeyValue
, we will try to parse the "delay" keyvalue. Keyvalues like "targetname" are handled automatically.
void CTargetTimer::KeyValue( KeyValueData* pkvd )
{
// Keyvalue parsing works by comparing the current keyvalue's name
// to the keyvalue name we are looking for, then we parse the value itself
if ( FStrEq( pkvd->szKeyName, "delay" ) )
{
m_flTimerDelay = atof( pkvd->szValue );
pkvd->fHandled = TRUE;
}
else
{ // In case no matching keyvalues are found, we must pass it on
// to the class we inherited from
CBaseEntity::KeyValue( pkvd );
}
}
Now that the boilerplate has been taken care of, you can start implementing the actual functionality.
Use
:
void CTargetTimer::Use( CBaseEntity* pActivator, CBaseEntity* pCaller, USE_TYPE useType, float value )
{
if ( IsEnabled() )
{
SetThink( nullptr );
}
else
{
SetThink( &CTargetTimer::TimerThink );
}
pev->nextthink = gpGlobals->time + m_flTimerDelay;
}
Alternatively, you can shorten the SetThink
calls to:
SetThink( IsEnabled() ? nullptr : &CTargetTimer::TimerThink );
Finally, you must define TimerThink
and IsEnabled
. TimerThink
is trivial, as it just triggers its targets and sets the next think time:
void CTargetTimer::TimerThink()
{
SUB_UseTargets( this, USE_TOGGLE, 0.0f );
pev->nextthink = gpGlobals->time + m_flTimerDelay;
}
SUB_UseTargets
is used by all entities that can trigger other entities. It essentially loops through all entities in the map with the matching target name.IsEnabled
is even simpler:
bool CTargetTimer::IsEnabled() const
{
// We could've used a BOOL variable to keep track of
// the entity's status, but this is a more optimal way
return m_pfnThink != nullptr;
}
Simply put, when the entity is inactive, m_pfnThink
is nullptr
. Otherwise, m_pfnThink
points to TimerThink
.
KeyValue
:
int Save( CSave& save );
int Restore( CRestore& restore );
static TYPEDESCRIPTION m_SaveData[];
And below LINK_ENTITY_TO_CLASS
:
TYPEDESCRIPTION CTargetTimer::m_SaveData[] =
{
DEFINE_FIELD( CTargetTimer, m_flTimerDelay, FIELD_FLOAT )
};
IMPLEMENT_SAVERESTORE( CTargetTimer, CBaseEntity );
trigger_auto
or something else to make this timer tick from the start.
#define SF_TIMER_START_ON 1
Alternatively, you can use constexpr
, or static constexpr
inside the class, but, to remain consistent with the SDK, we'll use a macro.pev->spawnflags
and it's trivial to check them:
if ( pev->spawnflags & SF_TIMER_START_ON )
With all that in mind, you will modify Spawn
like so:
void CTargetTimer::Spawn()
{
SetThink( nullptr );
if ( pev->spawnflags & SF_TIMER_START_ON )
{
// This will simply turn on the entity, as
// if it triggered itself
Use( this, this, USE_TOGGLE, 0.0f );
}
}
game_text
and target_timer
, with the intent of displaying text to an individual player every 30 seconds, the game will crash. This is because of the way we're calling SUB_UseTargets
. The first argument (this
) is the activator:
SUB_UseTargets( this, USE_TOGGLE, 0.0f );
One way you can work around this issue is to "grab" the activator in Use
, and store it somewhere. You will need to save-restore it as well:
// In the class
CBaseEntity* m_pActivator = nullptr;
...
// In Use
m_pActivator = pActivator;
...
// In the save-restore table
DEFINE_FIELD( CTargetTimer, m_pActivator, FIELD_CLASSPTR ),
Then modify TimerThink
to use m_pActivator
instead of this
as the activator.
minDelay
and maxDelay
// In the class
float m_flRandomMax = 0.0f;
...
// In the save-restore table
DEFINE_FIELD( CTargetTimer, m_flRandomMax, FIELD_FLOAT ),
...
// In KeyValue
else if ( FStrEq( pkvd->szKeyName, "random" ) )
{
m_flRandomMax = atof( pkvd->szValue );
pkvd->fHandled = TRUE;
}
Finally, you will modify TimerThink
to update pev->nextthink
with this new variable taken into account:
pev->nextthink = gpGlobals->time + m_flTimerDelay + RANDOM_FLOAT( -m_flRandomMax, m_flRandomMax );
m_flRandomMax
is greater than m_flDelayTime
. While not really a problem, you might still want to warn the mapper that the entity cannot randomly go back in time and trigger things twice.Spawn
like so:
void CTargetTimer::Spawn()
{
SetThink( nullptr );
if ( pev->spawnflags & SF_TIMER_START_ON )
{
// This will simply turn on the entity, as
// if it triggered itself
Use( this, this, USE_TOGGLE, 0.0f );
}
else if ( !pev->targetname )
{
ALERT( at_console, "target_timer with no name, deleting...\n" );
UTIL_Remove( this );
}
}
Notice how we're only checking for the missing targetname if the "start on" flag isn't set. If the "start on" flag is set, then the entity still does something, even without a targetname.
@PointClass base(Targetname, Target) = target_timer
[
delay(string) : "Delay (in seconds)" : "1"
random(string) : "Added random delay" : "0"
]
However, we're missing the spawnflags. Their syntax is a bit different:
spawnflags(Flags) =
[
1 : "Spawnflag 1" : 0
2 : "Spawnflag 2" : 1
4 : "Spawnflag 3" : 1
8 : "Spawnflag 4" : 0
]
According to this, spawnflags 2 and 3 (values 2 and 4) will be turned on by default. With this in mind, we'll add the "start on" spawnflag, so the final FGD entry will be:
@PointClass base(Targetname, Target) = target_timer
[
delay(string) : "Delay (in seconds)" : "1"
random(string) : "Added random delay" : "0"
spawnflags(Flags) =
[
1 : "Start on" : 0
]
]
Also notice that we're using the Targetname
and Target
bases. They automatically add the respective keyvalues to the entry.
You must log in to post a comment. You can login or register a new account.