Skip to content

Temp Entity Overview

WatIsDeze edited this page Dec 9, 2025 · 1 revision

Temporary Entity System

Temporary entities (temp entities) are short-lived visual effects that don't require persistent entity objects. They're essential for efficient visual feedback like bullet impacts, sparks, explosions, and blood splatter.

What Are Temporary Entities?

Temporary Entities are transient visual effects spawned at a specific location for a brief moment. Unlike regular entities:

  • No entity_state_t: Not part of the entity snapshot system
  • One-Time Events: Sent once, not tracked or updated
  • Client-Side Only: Server tells clients "spawn effect at X", clients handle rendering
  • Efficient: No ongoing network traffic or server-side management

When to Use Temp Entities

Use Temp Entities For:

  • Bullet impacts and ricochet sparks
  • Blood splatter from damage
  • Small explosions and particle bursts
  • Environmental effects (steam, sparks, bubbles)
  • Visual-only effects that last < 1 second

Don't Use Temp Entities For:

  • Projectiles that need physics (rockets, grenades) - use regular entities
  • Effects that persist > 1 second - use local entities
  • Anything that needs server-side logic - use regular entities
  • Player-attached effects - use entity events

Temp Entity Event Types

Location: src/baseq2rtxp/sharedgame/sg_tempentity_events.h

Available Event Types

typedef enum temp_entity_event_e {
    TE_GUNSHOT,             // Bullet impact (default surface)
    
    TE_BLOOD,               // Blood splatter (small)
    TE_MOREBLOOD,           // Blood splatter (large)
    
    TE_BUBBLETRAIL,         // Bubble trail underwater
    TE_BUBBLETRAIL2,        // Alternate bubble trail
    TE_SPLASH,              // Water splash (bullet hitting water)
    
    TE_STEAM,               // Steam puff
    TE_HEATBEAM_STEAM,      // Steam from heat beam
    
    TE_SPARKS,              // Generic sparks
    TE_HEATBEAM_SPARKS,     // Heat beam sparks
    TE_BULLET_SPARKS,       // Bullet ricochet sparks
    TE_ELECTRIC_SPARKS,     // Electrical sparks
    TE_LASER_SPARKS,        // Laser impact sparks
    TE_TUNNEL_SPARKS,       // Tunnel digging sparks
    TE_WELDING_SPARKS,      // Welding effect
    
    TE_FLAME,               // Flame burst
    
    TE_PLAIN_EXPLOSION,     // Generic explosion
    
    TE_TELEPORT_EFFECT,     // Teleporter effect
    
    TE_FLASHLIGHT,          // Flashlight beam
    TE_DEBUGTRAIL,          // Debug visualization trail
    
    TE_NUM_ENTITY_EVENTS    // Total count
} temp_entity_event_t;

Splash Types

For TE_SPLASH events:

static constexpr int32_t SPLASH_UNKNOWN     = 0;
static constexpr int32_t SPLASH_SPARKS      = 1;  // Metal surface
static constexpr int32_t SPLASH_BLUE_WATER  = 2;  // Clean water
static constexpr int32_t SPLASH_BROWN_WATER = 3;  // Dirty water
static constexpr int32_t SPLASH_SLIME       = 4;  // Toxic slime
static constexpr int32_t SPLASH_LAVA        = 5;  // Molten lava
static constexpr int32_t SPLASH_BLOOD       = 6;  // Blood pool

Spawning Temp Entities (Server-Side)

Basic Pattern

// 1. Write message type
gi.WriteByte(svc_temp_entity);

// 2. Write temp entity event type
gi.WriteByte(TE_GUNSHOT);

// 3. Write event-specific data
gi.WritePosition(origin);       // Where effect spawns
gi.WriteDir(direction);         // Optional: effect direction

// 4. Send to clients
gi.multicast(origin, MULTICAST_PVS);

Multicast Modes

MULTICAST_ALL       // Send to all clients (expensive!)
MULTICAST_PHS       // Send to clients in Potentially Hearable Set
MULTICAST_PVS       // Send to clients in Potentially Visible Set (most common)
MULTICAST_ALL_R     // Reliable to all clients
MULTICAST_PHS_R     // Reliable to PHS
MULTICAST_PVS_R     // Reliable to PVS

Tip: Use MULTICAST_PVS for visual effects - only clients who can see the location receive it.

Common Temp Entity Examples

Bullet Impact

void SVG_BulletImpact(vec3_t origin, vec3_t normal, int surfaceFlags) {
    // Don't show impact on sky
    if (surfaceFlags & CM_SURFACE_FLAG_SKY)
        return;
        
    // Spawn bullet sparks
    gi.WriteByte(svc_temp_entity);
    gi.WriteByte(TE_BULLET_SPARKS);
    gi.WritePosition(origin);
    gi.WriteDir(normal);
    gi.multicast(origin, MULTICAST_PVS);
    
    // Also spawn decal (if on solid surface)
    if (!(surfaceFlags & CM_SURFACE_FLAG_WARP)) {
        SpawnBulletDecal(origin, normal);
    }
}

Blood Splatter

void SVG_BloodEffect(vec3_t origin, vec3_t dir, int damage) {
    temp_entity_event_t type;
    
    // More blood for higher damage
    if (damage < 10) {
        type = TE_BLOOD;
    } else {
        type = TE_MOREBLOOD;
    }
    
    gi.WriteByte(svc_temp_entity);
    gi.WriteByte(type);
    gi.WritePosition(origin);
    gi.WriteDir(dir);
    gi.multicast(origin, MULTICAST_PVS);
}

Water Splash

void SVG_WaterSplash(vec3_t origin, vec3_t dir, int count, int splashType) {
    gi.WriteByte(svc_temp_entity);
    gi.WriteByte(TE_SPLASH);
    gi.WriteByte(count);            // Number of particles
    gi.WritePosition(origin);
    gi.WriteDir(dir);
    gi.WriteByte(splashType);       // SPLASH_BLUE_WATER, etc.
    gi.multicast(origin, MULTICAST_PVS);
}

// Usage
SVG_WaterSplash(waterSurface, vec3_up, 8, SPLASH_BLUE_WATER);

Explosion

void SVG_Explosion(vec3_t origin) {
    gi.WriteByte(svc_temp_entity);
    gi.WriteByte(TE_PLAIN_EXPLOSION);
    gi.WritePosition(origin);
    gi.multicast(origin, MULTICAST_PHS);  // Use PHS for sound
    
    // Also spawn explosion sprite entity
    auto *explosion = SVG_Spawn<svg_misc_explosion_t>();
    explosion->s.origin = origin;
    explosion->Spawn();
}

Sparks

void SVG_SparkShower(vec3_t origin, vec3_t dir, int count) {
    gi.WriteByte(svc_temp_entity);
    gi.WriteByte(TE_SPARKS);
    gi.WritePosition(origin);
    gi.WriteDir(dir);
    gi.WriteByte(count);  // Number of sparks
    gi.multicast(origin, MULTICAST_PVS);
}

// Electric sparks (different visual)
void SVG_ElectricSparks(vec3_t origin) {
    gi.WriteByte(svc_temp_entity);
    gi.WriteByte(TE_ELECTRIC_SPARKS);
    gi.WritePosition(origin);
    gi.multicast(origin, MULTICAST_PVS);
}

Steam

void SVG_Steam(vec3_t origin, vec3_t dir, int count, int speed, 
               int noise, int basecount) {
    gi.WriteByte(svc_temp_entity);
    gi.WriteByte(TE_STEAM);
    gi.WritePosition(origin);
    gi.WriteDir(dir);
    gi.WriteByte(count);       // Particle count
    gi.WriteByte(speed);       // Movement speed
    gi.WriteByte(noise);       // Random spread
    gi.WriteByte(basecount);   // Additional count modifier
    gi.multicast(origin, MULTICAST_PVS);
}

// Example: Steam vent
SVG_Steam(ventOrigin, vec3_up, 10, 50, 8, 0);

Teleport Effect

void SVG_TeleportEffect(vec3_t origin) {
    gi.WriteByte(svc_temp_entity);
    gi.WriteByte(TE_TELEPORT_EFFECT);
    gi.WritePosition(origin);
    gi.multicast(origin, MULTICAST_PVS);
    
    // Also play teleport sound
    gi.sound(nullptr, CHAN_AUTO, gi.soundindex("world/teleport.wav"),
             1.0f, ATTN_NORM, 0);
}

Client-Side Processing

Location: src/baseq2rtxp/clgame/clg_temp_entities.cpp

Temp Entity Handler

void CLG_ParseTempEntity(void) {
    int type = MSG_ReadByte(&net_message);
    
    switch (type) {
        case TE_GUNSHOT:
            CLG_GunShotEffect();
            break;
            
        case TE_BLOOD:
            CLG_BloodEffect(false);  // Small blood
            break;
            
        case TE_MOREBLOOD:
            CLG_BloodEffect(true);   // Large blood
            break;
            
        case TE_BULLET_SPARKS:
            CLG_BulletSparkEffect();
            break;
            
        case TE_SPLASH:
            CLG_SplashEffect();
            break;
            
        // ... handle all temp entity types ...
    }
}

Example: Bullet Sparks

void CLG_BulletSparkEffect(void) {
    // Read data from message
    vec3_t origin = MSG_ReadPos(&net_message);
    vec3_t dir = MSG_ReadDir(&net_message);
    
    // Spawn particle effect
    for (int i = 0; i < 6; i++) {
        clg_particle_t *p = CLG_AllocParticle();
        if (!p)
            return;
            
        p->type = PARTICLE_SPARK;
        p->color = 0xe0 + (rand() & 7);  // Yellow-orange
        p->lifetime = 0.6f + frand() * 0.2f;
        
        // Randomize velocity
        p->vel[0] = dir[0] * 40 + crand() * 10;
        p->vel[1] = dir[1] * 40 + crand() * 10;
        p->vel[2] = dir[2] * 40 + crand() * 10;
        
        p->org = origin;
        p->gravity = PARTICLE_GRAVITY;
    }
    
    // Play ricochet sound
    S_StartSound(origin, -1, CHAN_AUTO, S_RegisterSound("weapons/ric1.wav"),
                 0.5f, ATTN_STATIC, 0);
}

Example: Blood Effect

void CLG_BloodEffect(bool large) {
    vec3_t origin = MSG_ReadPos(&net_message);
    vec3_t dir = MSG_ReadDir(&net_message);
    
    int count = large ? 20 : 10;
    
    for (int i = 0; i < count; i++) {
        clg_particle_t *p = CLG_AllocParticle();
        if (!p)
            return;
            
        p->type = PARTICLE_BLOOD;
        p->color = 0xe8;  // Red
        p->lifetime = 0.8f + frand() * 0.4f;
        p->scale = large ? 6.0f : 4.0f;
        
        // Spray in direction of hit
        p->vel[0] = dir[0] * 60 + crand() * 40;
        p->vel[1] = dir[1] * 60 + crand() * 40;
        p->vel[2] = dir[2] * 60 + crand() * 40;
        
        p->org = origin + vec3_t{crand(), crand(), crand()} * 2;
        p->gravity = PARTICLE_GRAVITY * 2;  // Falls faster
    }
    
    // Spawn blood decal
    CLG_SpawnDecal(origin, dir, DECAL_BLOOD);
}

Practical Usage Patterns

In Weapon Fire

void svg_weapon_shotgun_t::Fire() {
    vec3_t start = owner->s.origin + viewHeight;
    vec3_t dir;
    AngleVectors(owner->client->v_angle, dir, nullptr, nullptr);
    
    // Fire multiple pellets
    for (int i = 0; i < 12; i++) {
        vec3_t spread = ApplySpread(dir, 500, 500);
        
        cm_trace_t tr = SVG_Trace(start, vec3_origin, vec3_origin,
                                   start + spread * 2048,
                                   owner, MASK_SHOT);
        
        if (tr.fraction < 1.0f) {
            // Hit something
            svg_base_edict_t *hit = tr.ent;
            
            // Damage
            if (hit->takedamage) {
                SVG_Damage(hit, owner, owner, dir, tr.endpos,
                           tr.plane.normal, damage, kick, 
                           DAMAGE_BULLET, MOD_SHOTGUN);
            }
            
            // Visual effect
            if (hit->takedamage) {
                // Blood on flesh
                gi.WriteByte(svc_temp_entity);
                gi.WriteByte(TE_BLOOD);
                gi.WritePosition(tr.endpos);
                gi.WriteDir(tr.plane.normal);
                gi.multicast(tr.endpos, MULTICAST_PVS);
            } else {
                // Sparks on hard surface
                gi.WriteByte(svc_temp_entity);
                gi.WriteByte(TE_BULLET_SPARKS);
                gi.WritePosition(tr.endpos);
                gi.WriteDir(tr.plane.normal);
                gi.multicast(tr.endpos, MULTICAST_PVS);
            }
        }
    }
}

In Damage Calculation

void SVG_Damage(svg_base_edict_t *targ, /*...*/) {
    // Apply damage...
    targ->health -= damage;
    
    // Blood effect if flesh
    if (targ->takedamage && damage > 0) {
        temp_entity_event_t bloodType = (damage > 20) ? TE_MOREBLOOD : TE_BLOOD;
        
        gi.WriteByte(svc_temp_entity);
        gi.WriteByte(bloodType);
        gi.WritePosition(point);
        gi.WriteDir(normal);
        gi.multicast(point, MULTICAST_PVS);
    }
    
    // Check for death...
}

In Environmental Effects

class svg_misc_steam_t : public svg_base_edict_t {
    void Think() override {
        // Emit steam puff
        vec3_t dir = {0, 0, 1};  // Upward
        
        gi.WriteByte(svc_temp_entity);
        gi.WriteByte(TE_STEAM);
        gi.WritePosition(s.origin);
        gi.WriteDir(dir);
        gi.WriteByte(8);   // count
        gi.WriteByte(50);  // speed
        gi.WriteByte(8);   // noise
        gi.WriteByte(0);   // basecount
        gi.multicast(s.origin, MULTICAST_PVS);
        
        // Think again in 0.5 seconds
        nextThinkTime = level.time + gametime_t::from_sec(0.5f);
    }
};

Creating Custom Temp Entities

To add a new temp entity type:

1. Add Event Type

In src/baseq2rtxp/sharedgame/sg_tempentity_events.h:

typedef enum temp_entity_event_e {
    // ... existing types ...
    
    TE_CUSTOM_EFFECT,      // Your new effect
    
    TE_NUM_ENTITY_EVENTS
} temp_entity_event_t;

2. Server-Side Spawning

void SVG_CustomEffect(vec3_t origin, vec3_t color, int intensity) {
    gi.WriteByte(svc_temp_entity);
    gi.WriteByte(TE_CUSTOM_EFFECT);
    gi.WritePosition(origin);
    gi.WriteByte(color[0] * 255);
    gi.WriteByte(color[1] * 255);
    gi.WriteByte(color[2] * 255);
    gi.WriteByte(intensity);
    gi.multicast(origin, MULTICAST_PVS);
}

3. Client-Side Handler

In src/baseq2rtxp/clgame/clg_temp_entities.cpp:

void CLG_ParseTempEntity(void) {
    int type = MSG_ReadByte(&net_message);
    
    switch (type) {
        // ... existing cases ...
        
        case TE_CUSTOM_EFFECT:
            CLG_CustomEffect();
            break;
    }
}

void CLG_CustomEffect(void) {
    vec3_t origin = MSG_ReadPos(&net_message);
    vec3_t color = {
        MSG_ReadByte(&net_message) / 255.0f,
        MSG_ReadByte(&net_message) / 255.0f,
        MSG_ReadByte(&net_message) / 255.0f
    };
    int intensity = MSG_ReadByte(&net_message);
    
    // Spawn particles, lights, sounds, etc.
    for (int i = 0; i < intensity; i++) {
        clg_particle_t *p = CLG_AllocParticle();
        // ... configure particle ...
    }
}

Performance Tips

Bandwidth Consideration

// BAD: Spawns temp entity every frame
void Think() {
    SpawnSparks(s.origin);
    nextThinkTime = level.time + FRAMETIME;  // 40 Hz
}

// GOOD: Spawns periodically
void Think() {
    SpawnSparks(s.origin);
    nextThinkTime = level.time + gametime_t::from_sec(0.25f);  // 4 Hz
}

PVS Culling

Always use MULTICAST_PVS for visual effects:

// Automatically culled to visible clients
gi.multicast(origin, MULTICAST_PVS);

Particle Budget

Don't spawn too many particles:

// Limit particle count
int maxParticles = min(damageAmount / 5, 20);
for (int i = 0; i < maxParticles; i++) {
    SpawnBloodParticle();
}

Debugging

Console Variables

cl_show_temp_entities 1     // Show temp entity spawns
cl_temp_entity_debug 1      // Print temp entity info
r_drawparticles 1           // Show particles (default)

Debug Visualization

#ifdef _DEBUG
void SVG_DebugTempEntity(const char *name, vec3_t origin) {
    gi.dprintf("TE: %s at (%.1f, %.1f, %.1f)\n", 
               name, origin[0], origin[1], origin[2]);
}
#endif

// Usage
SVG_DebugTempEntity("BLOOD", hitPos);
gi.WriteByte(svc_temp_entity);
gi.WriteByte(TE_BLOOD);
// ...

Next Steps


Previous: Entity System Overview
See Also: API Reference - Temp Entity Events

Clone this wiki locally