player.cpp
in the server project) in any "standard" Half-Life SDK, you might have seen this:
BOOL CBasePlayer::FlashlightIsOn()
{
return FBitSet( pev->effects, EF_DIMLIGHT );
}
void CBasePlayer::FlashlightTurnOn()
{
// Game rules check if flashlight is allowed here
if ( (pev->weapons & (1 << WEAPON_SUIT)) )
{
// Flashlight turn on sound code here
SetBits( pev->effects, EF_DIMLIGHT );
// Client message and timing code here
}
}
void CBasePlayer::FlashlightTurnOff()
{
// Flashlight turn off sound code here
ClearBits( pev->effects, EF_DIMLIGHT );
// Client message and timing code here
}
The server basically set, check and clear the EF_DIMLIGHT
bit in the effects
variable inside the player's private entity variable (pev
) structure.EF_DIMLIGHT
related.#define EF_DIMLIGHT 8
declaration in the common/const.h
file and the usages in the code above, you won't find anything in the client/server projects.EF_DIMLIGHT
in Quake's code, there is an interesting part in QW/client/cl_ents.c
(link here):
void CL_LinkPacketEntities()
{
// Local variables declaration here
for ( pnum=0 ; pnum < pack->num_entities ; pnum++ ) // Iterate through all "packet entities"
{
s1 = &pack->entities[pnum];
s2 = s1; // FIXME: no interpolation right now
// spawn light flashes, even ones coming from invisible objects
if ( (s1->effects & (EF_BLUE | EF_RED)) == (EF_BLUE | EF_RED) )
CL_NewDlight( s1->number, s1->origin[0], s1->origin[1], s1->origin[2], 200 + (rand() & 31), 0.1, 3 );
else if ( s1->effects & EF_BLUE )
CL_NewDlight( s1->number, s1->origin[0], s1->origin[1], s1->origin[2], 200 + (rand() & 31), 0.1, 1 );
else if ( s1->effects & EF_RED )
CL_NewDlight( s1->number, s1->origin[0], s1->origin[1], s1->origin[2], 200 + (rand() & 31), 0.1, 2 );
else if ( s1->effects & EF_BRIGHTLIGHT )
CL_NewDlight( s1->number, s1->origin[0], s1->origin[1], s1->origin[2] + 16, 400 + (rand() & 31), 0.1, 0 );
else if ( s1->effects & EF_DIMLIGHT )
CL_NewDlight( s1->number, s1->origin[0], s1->origin[1], s1->origin[2], 200 + (rand() & 31), 0.1, 0 );
// Rest of the code like visibility checks, setup (model, body, skin...) and handling (origin, rotation, flags...) here
}
}
Looks like a new client dynamic light is created for 0,1 second for all entities that have the EF_DIMLIGHT
flag set which makes sense for now. We can also assume CL_LinkPacketEntities
is called every frame. If we take a deeper look at what CL_NewDlight
does, we can see this (link here):
void CL_NewDlight (int key, float x, float y, float z, float radius, float time, int type)
{
dlight_t *dl;
dl = CL_AllocDlight( key );
dl->origin[0] = x;
dl->origin[1] = y;
dl->origin[2] = z;
dl->radius = radius;
dl->die = cl.time + time;
if (type == 0) {
dl->color[0] = 0.2;
dl->color[1] = 0.1;
dl->color[2] = 0.05;
dl->color[3] = 0.7;
} // Other types here
}
Good eyes will notice that the color and radius does not match Half-Life's flashlight. Remember that we are looking at Quake's code, not Half-Life's so there is going to be inaccuracies between the code of the two games.dlight_t
on the client project, we're going to make a "workaround" to not use the engine code and basically make our own.
player.h
(server project), we add this:
enum class FlashlightClientState
{
Disabled,
Enabled,
ForceUpdate
};
class CBasePlayer : public CBaseMonster
{
// [...]
bool m_bIsFlashlightOn;
FlashlightClientState m_eClientIsFlashlightOn;
}
The bool
itself is self-explanatory. The reason we have the enum class FlashlightClientState
is to synchronize the client with the server and the reason this is not a bool
but an enum class
with a ForceUpdate
value is that you have no guarantee that the flashlight will either be off or on when loading a saved game. That's the purpose of ForceUpdate
, to retrieve the actual state of the flashlight and send it to the client (for the HUD and the light that we will add later on).FlashlightClientState
is not "tied" to the flashlight. In other words, you could use the same enum class
for another bool
that has no relation to the flashlight.
As mentioned in the warning: our new bool m_bIsFlashlightOn
variable need to be saved/restored. So don't forget that DEFINE_FIELD( CBasePlayer, m_bIsFlashlightOn, FIELD_BOOLEAN )
line in the CBasePlayer
save/restore table.bool CBasePlayer::FlashlightIsOn()
, void CBasePlayer::FlashlightTurnOn()
and void CBasePlayer::FlashlightTurnOff()
methods.
bool CBasePlayer::FlashlightIsOn()
: Remove the existing code and just return m_bIsFlashlightOn;
.void CBasePlayer::FlashlightTurnOn()
: Remove the SetBits( pev->effects, EF_DIMLIGHT );
line and replace it by m_bIsFlashlightOn = true;
. Remove the code that send the gmsgFlashlight
message to the client as well, this is going to be handled differently later.CBasePlayer::FlashlightTurnOff()
: Remove ClearBits( pev->effects, EF_DIMLIGHT );
and replace with m_bIsFlashlightOn = false;
and the message is removed as well.m_iClientBattery
).void CBasePlayer::Spawn()
, void CBasePlayer::Precache()
and void CBasePlayer::ForceClientDllUpdate()
, we're going to force a synchronization whenever the player is spawning, precaching and the client DLL requires an update (loading/saving a saved game as well as transitions between maps). So add m_eClientIsFlashlightOn = FlashlightClientState::ForceUpdate;
at these 3 locations.void CBasePlayer::UpdateClientData()
, just above the code that check if you are using a train you can add this bit:
if ( m_eClientIsFlashlightOn == FlashlightClientState::ForceUpdate ||
(m_bIsFlashlightOn && m_eClientIsFlashlightOn == FlashlightClientState::Disabled) ||
(!m_bIsFlashlightOn && m_eClientIsFlashlightOn == FlashlightClientState::Enabled) )
{
MESSAGE_BEGIN( MSG_ONE, gmsgFlashlight, NULL, pev );
WRITE_BYTE( m_bIsFlashlightOn ? 1 : 0 );
WRITE_BYTE( m_iFlashBattery );
MESSAGE_END();
m_eClientIsFlashlightOn = m_bIsFlashlightOn ? FlashlightClientState::Enabled : FlashlightClientState::Disabled;
}
Here we basically check if we are forced to update or if the flashlight server state does not match the client one. If that's the case, we send the message to the client and keep track of what has been sent to the client.
Compile your code, put the updated binaries in your mod and start a map. Play with the flashlight (turn it on, turn it off, save and load games for singleplayer projects) for a little bit.hud.h
and more precisely: the CHudFlashlight
class. We can declare the dynamic light there (dlight_t *m_pLight;
). To make the code cleaner, we will declare a void UpdateFlashlight()
method as well and put our new code in there.flashlight.cpp
, just before returning true
in the Draw( float flTime )
method, we call our newly UpdateFlashlight()
method.void CHudFlashlight::UpdateFlashlight()
does. The code being quite complex than what you did earlier, here's the full code of said method (and please read the comments to understand why).
void CHudFlashlight::UpdateFlashlight()
{
if ( !m_fOn ) // The flashlight is off
{
if ( m_pLight ) // We still have our light
{
memset( m_pLight, 0, sizeof( *m_pLight ) ); // Reset it's data thus turning it off (thanks Solokiller for the reference to Quake's code)
m_pLight = nullptr; // De-reference it
}
return; // No need to go any further since the flashlight is off
}
cl_entity_t *pPlayer = gEngfuncs.GetLocalPlayer(); // Get the local player
if ( !pPlayer )
return;
Vector vecAngles; // Get it's view angles
gEngfuncs.GetViewAngles( vecAngles );
Vector vecForward; // Get his "forward direction" (or where he's aiming at)
AngleVectors( vecAngles, vecForward, nullptr, nullptr );
Vector vecEnd; // Let's have a range of 2048 Hammer units
VectorMA( pPlayer->origin, 2048.0f, vecForward, vecEnd );
pmtrace_t tr; // Make a trace between the player and the max. range, we don't want the flashlight to go through the world and creating it outside of the world
// You will likely need to include both "pm_defs.h" and "event_api.h" to use these
gEngfuncs.pEventAPI->EV_SetSolidPlayers( pPlayer->index - 1 );
gEngfuncs.pEventAPI->EV_SetTraceHull( 2 );
gEngfuncs.pEventAPI->EV_PlayerTrace( pPlayer->origin, vecEnd, PM_STUDIO_BOX, -1, &tr );
if ( !m_pLight ) // The flashlight was off?
{
m_pLight = gEngfuncs.pEfxAPI->CL_AllocDlight( 1 ); // Create the light with the ID #1 (0 is already used for muzzle flashes)
m_pLight->die = FLT_MAX; // We don't want the light to "die" right away so we use a very long duration as a "hack" (FLT_MAX need the system's "float.h" include)
}
m_pLight->origin = tr.endpos; // Put the light at the end of the trace
m_pLight->radius = 180.0f; // The radius in Hammer units of the light, 180 is almost the double or maybe x2.5 of Half-Life flashlight's size
m_pLight->color.r = 255.0f; // Flashlights are usually white, but you want a pink/red/green/purple/magenta/blue/orange or whatever color? Go ahead and update the amount of RGB
m_pLight->color.g = 255.0f;
m_pLight->color.b = 255.0f;
}
Compile the binaries, put them in your mod and test the flashlight. You should now be able to see the light where you are pointing at when the flashlight is on. The light should also be killed when you turn the flashlight off.GetFlashlightRadius
returning a float
(to keep the code clean) that would look like this:
float CHudFlashlight::GetFlashlightRadius( const Vector vecSrc, const pmtrace_t tr )
{
const float flDist = fabs( (tr.endpos - vecSrc).Length() ); // Calculate the distance between the player and where the flashlight is pointed to
if ( flDist > 512.0f ) // There is more than 512 Hammer units of distance, assume the full radius
return 180.0f;
// There is less than 512 Hammer units, divide the distance by the short range to get a multiplier between 0 and 1 and use it on the full radius
// NEVER use a radius of 0 because that's going to kill the light!
return fmax( 0.01f, 180.0f * (flDist / 512.0f) );
}
Assign the result of the call to our new function to m_pLight->radius
while passing the player origin (pPlayer->origin
) and the result of the trace (tr
).You must log in to post a comment. You can login or register a new account.