Entity Programming - Temporary Entity Effects Last edited 1 year ago2023-08-16 19:35:57 UTC

You are viewing an older revision of this wiki page. The current revision may be more detailed and up-to-date. Click here to see the current revision of this page.

Half-Life Programming

On this page you will learn a bit about temporary entities in Half-Life SDK, how to use them and how the SDK uses them.

Introduction

At some point in your modding journey, you may feel the need to spice up your mod with special particle effects. You can approximate them by spawning lots of sprite entities and manipulating them, whether you change their velocity periodically and/or opacity, or whatever else.

However, you'll soon find out how that kind of approach will result in 1) running out of edicts 2) running out of visible entities; and in some cases 3) the game may even crash. In multiplayer, that would use a ton of bandwidth, too. After all, that is indeed what happens when you use the serverside to perform something meant for the clientside.

Fortunately, the engine has an API that will effectively do just that: send networked messages to nearby clients, so they locally spawn the effects by themselves. They are called, well, messages. Temporary entities are a subset of these server-client messages.

The structure of a message

Temporary entity messages (temporary entities from now on) are generally written like so:
// Initiate the message
MESSAGE_BEGIN( messageDestination, SVC_TEMPENTITY, origin );

// Send data relevant to the message
WRITE_BYTE( temporaryEntityId );
WRITE_XYZ( data );

// Mark the end of the message
MESSAGE_END();
For example, a smoke sprite effect:
MESSAGE_BEGIN( MSG_PVS, SVC_TEMPENTITY, pev->origin );

WRITE_BYTE( TE_SMOKE );
WRITE_COORD( pev->origin.x );
WRITE_COORD( pev->origin.y );
WRITE_COORD( pev->origin.z );
WRITE_SHORT( m_SmokeSprite ); // smoke sprite index
WRITE_BYTE( 50 );            // 5x scale
WRITE_BYTE( 10 );            // 10fps

MESSAGE_END();
Let's look at MESSAGE_BEGIN a bit better:
inline void MESSAGE_BEGIN(int msg_dest, int msg_type, const float* pOrigin = NULL, edict_t* ed = NULL)
{
    (*g_engfuncs.pfnMessageBegin)(msg_dest, msg_type, pOrigin, ed);
}
It has a few parameters, some of which are optional:
1) msg_dest - the destination of the message. The message may be sent to all clients, a few, or an individual, through reliable or unreliable channels. It can be one of the following: 2) msg_type - Type of the message. There are generally a few built-in ones: 3) pOrigin - The location/position the message is emitted from, used by MSG_PVS, MSG_PAS and similar. It's unnecessary to set it if you're using MSG_ALL and such.

4) ed - The edict of the target client. In other words, the client whom this message will be sent to.

We're not done yet, though. Once you begin a message, you need to put some data in it too.
We'll only focus on SVC_TEMPENTITY messages, since those are most used, and perhaps the most interesting ones.

Choosing the right destination type

It is fairly important to choose the right destination type. For instance, if you were using MSG_ALL everywhere, you would effectively broadcast everything that ever happened, to every client in the map. This is far from optimal. There are generally a few guidelines you can follow here:

Constructing a temporary entity message

The first thing that goes after MESSAGE_BEGIN should be something like this:
WRITE_BYTE( TE_SOMETHING );
This TE_SOMETHING would be replaced by one of the appropriate temporary entity IDs, such as TE_SMOKE. In const.h, you can find all of them:
#define TE_BEAMPOINTS 0 // beam effect between two points
#define TE_BEAMENTPOINT 1 // beam effect between point and entity
#define TE_GUNSHOT 2 // particle effect plus ricochet sound
...
There are too many of them to list here. Also note that some of these temporary entities are not used at all in the SDK and/or broken. However, there is example code on how to use the ones that do work, as well as a video demonstrating how each looks.
TODO: list the broken and unused temporary entity types.
Each temporary entity type has its own parameters, and they are documented in const.h. Here is an example:
#define TE_SPRITE 17 // additive sprite, plays 1 cycle
// coord, coord, coord (position)
// short (sprite index)
// byte (scale in 0.1's)
// byte (brightness)
So in order to use TE_SPRITE, you would need to: And of course, end it with MESSAGE_END();.

Examples in the SDK

Here are some example usages of temporary entities:

Explosion

ggrenade.cpp has a nice example of a complete explosion message in CGrenade::Explode:
int iContents = UTIL_PointContents( pev->origin );

MESSAGE_BEGIN( MSG_PAS, SVC_TEMPENTITY, pev->origin );
WRITE_BYTE( TE_EXPLOSION );    // This makes a dynamic light and the explosion sprites/sound
WRITE_COORD( pev->origin.x ); // Send to PAS because of the sound
WRITE_COORD( pev->origin.y );
WRITE_COORD( pev->origin.z );
if ( iContents != CONTENTS_WATER )
{
    WRITE_SHORT( g_sModelIndexFireball );
}
else
{
    WRITE_SHORT( g_sModelIndexWExplosion );
}
WRITE_BYTE( (pev->dmg - 50) * .60 ); // scale * 10
WRITE_BYTE( 15 );                       // framerate
WRITE_BYTE( TE_EXPLFLAG_NONE );
MESSAGE_END();
Notice how, depending on whether the grenade is under water or not, a different sprite is used. Also note that this is only the visual aspect of the explosion. Damage to nearby entities is done separately.

Rocket trail

In rpg.cpp you may find how to initiate trails, in CRpgRocket::IgniteThink:
MESSAGE_BEGIN( MSG_BROADCAST, SVC_TEMPENTITY );

WRITE_BYTE( TE_BEAMFOLLOW );
WRITE_SHORT( entindex() ); // entity
WRITE_SHORT( m_iTrail );     // model
WRITE_BYTE( 40 );             // life
WRITE_BYTE( 5 );             // width
WRITE_BYTE( 224 );         // r, g, b
WRITE_BYTE( 224 );         // r, g, b
WRITE_BYTE( 255 );         // r, g, b
WRITE_BYTE( 255 );         // brightness

MESSAGE_END();
You may think this message is sent every once in a while, but actually it is only done when the rocket spawns. TE_BEAMFOLLOW will simply make it so that trails are emitted until an entity stops moving.

Decal placement

In world.cpp, in CDecal::TriggerDecal, you will find this:
UTIL_TraceLine( pev->origin - Vector( 5, 5, 5 ), pev->origin + Vector( 5, 5, 5 ), ignore_monsters, ENT( pev ), &trace );

MESSAGE_BEGIN( MSG_BROADCAST, SVC_TEMPENTITY );

WRITE_BYTE( TE_BSPDECAL );
WRITE_COORD( pev->origin.x );
WRITE_COORD( pev->origin.y );
WRITE_COORD( pev->origin.z );
WRITE_SHORT( (int)pev->skin ); // decal index
entityIndex = (short)ENTINDEX( trace.pHit );
WRITE_SHORT( entityIndex ); // entity index on which the decal will be placed
if ( 0 != entityIndex )
    WRITE_SHORT( (int)VARS( trace.pHit )->modelindex ); // if not worldspawn, place it on this BSP model

MESSAGE_END();
It's important to note that pev->origin here should be just a little bit above ground, maybe a couple units. The decal index is obtained like so:
pev->skin = DECAL_INDEX( pkvd->szValue );
The engine will look through decals.wad and find the right decal index there.
TODO: confirm this.

Conclusion

That is more or less it about temporary entities. You can now easily create neat visual effects with them. Some are more temporary than others, but at the end of the day, they will save you a good amount of edicts.

Comments

You must log in to post a comment. You can login or register a new account.