Skip to content

Using Temp Entities

WatIsDeze edited this page Dec 9, 2025 · 1 revision

Using Temporary Entities

This guide explains how to spawn and use temporary entities in your Q2RTXPerimental game code. Temporary entities are network-efficient, one-shot visual effects.

Quick Start

Basic Temp Entity Spawn

// Include the temp entity header
#include "sharedgame/sg_tempentity_events.h"

void Fire_Bullet(const vec3_t &impact_point, const vec3_t &surface_normal) {
    // 1. Write the temp entity message type
    gi.WriteByte(svc_temp_entity);
    
    // 2. Write the specific temp entity event
    gi.WriteByte(TE_GUNSHOT);
    
    // 3. Write event-specific parameters
    gi.WritePosition(impact_point);
    gi.WriteDir(surface_normal);
    
    // 4. Send to clients that can see this location
    gi.multicast(impact_point, MULTICAST_PVS);
}

That's it! The client will receive this message and spawn the appropriate visual effect.

The Message Writing API

gi.WriteByte(value)

Writes a single byte (0-255) to the network message.

gi.WriteByte(svc_temp_entity);  // Message type
gi.WriteByte(TE_GUNSHOT);       // Event type
gi.WriteByte(particle_count);    // Parameter

gi.WritePosition(vec3_t)

Writes a 3D position with compression (16-bit per axis).

vec3_t explosion_pos = {128.0f, 256.0f, 64.0f};
gi.WritePosition(explosion_pos);

Precision: ~1/8 unit (0.125 unit) accuracy

Range: -4096 to +4096 units per axis

gi.WriteDir(vec3_t)

Writes a normalized direction vector (compressed to 1 byte).

vec3_t surface_normal = {0.0f, 0.0f, 1.0f};  // Pointing up
gi.WriteDir(surface_normal);

Note: Direction must be normalized (length = 1.0) for proper encoding.

gi.WriteShort(value)

Writes a 16-bit integer (-32768 to 32767).

gi.WriteShort(particle_count);  // For effects that need counts > 255

gi.WriteString(text)

Writes a null-terminated string.

gi.WriteString("sound/weapons/shotgun.wav");

Rarely used for temp entities (more common for other message types).

Multicast Functions

After writing a temp entity message, you must send it using gi.multicast():

void gi.multicast(const vec3_t &origin, multicast_t to);

Multicast Types

MULTICAST_PVS (Most Common)

Potentially Visible Set - Send to clients that might see this location.

gi.multicast(impact_point, MULTICAST_PVS);

Use for:

  • Visual effects (sparks, blood, impacts)
  • Effects with accompanying sounds that are < 512 units range
  • Most temp entities

Characteristics:

  • Based on BSP visibility data
  • Very efficient
  • Clients around corners won't receive (unless visible)

MULTICAST_PHS

Potentially Hearable Set - Send to clients that might hear this location.

gi.multicast(explosion_origin, MULTICAST_PHS);

Use for:

  • Loud explosions
  • Sounds with > 512 unit attenuation
  • Effects that can be heard through walls

Characteristics:

  • Larger area than PVS
  • Still efficient
  • Better for audio-focused events

MULTICAST_ALL

All Clients - Send to every connected client.

gi.multicast(vec3_origin, MULTICAST_ALL);

Use for:

  • Global announcements
  • Server-wide events
  • Effects everyone should see regardless of location

Warning: Expensive! Use sparingly.

MULTICAST_ALL_R

All Clients, Reliable - Guaranteed delivery to all clients.

gi.multicast(vec3_origin, MULTICAST_ALL_R);

Use for:

  • Critical events that must be seen
  • Game state changes

Warning: Very expensive! Almost never needed for temp entities.

Common Temp Entity Patterns

Bullet Impact

void Fire_Bullet_Impact(const vec3_t &point, const vec3_t &normal, 
                         int surface_type) {
    gi.WriteByte(svc_temp_entity);
    
    // Choose effect based on surface
    if (surface_type == SURF_METAL) {
        gi.WriteByte(TE_BULLET_SPARKS);
    } else {
        gi.WriteByte(TE_GUNSHOT);
    }
    
    gi.WritePosition(point);
    gi.WriteDir(normal);
    gi.multicast(point, MULTICAST_PVS);
}

Blood Splatter

void Spawn_Blood(const vec3_t &origin, const vec3_t &dir, int damage) {
    gi.WriteByte(svc_temp_entity);
    
    // More blood for higher damage
    if (damage > 50) {
        gi.WriteByte(TE_MOREBLOOD);
    } else {
        gi.WriteByte(TE_BLOOD);
    }
    
    gi.WritePosition(origin);
    gi.WriteDir(dir);
    gi.multicast(origin, MULTICAST_PVS);
}

Water Splash

void Water_Splash(const vec3_t &origin, int liquid_type) {
    gi.WriteByte(svc_temp_entity);
    gi.WriteByte(TE_SPLASH);
    
    // Particle count
    gi.WriteByte(8);
    
    gi.WritePosition(origin);
    gi.WriteDir(vec3_up);  // Splash upward
    
    // Liquid type (SPLASH_BLUE_WATER, SPLASH_SLIME, etc.)
    gi.WriteByte(liquid_type);
    
    gi.multicast(origin, MULTICAST_PVS);
}

Explosion

void Create_Explosion(const vec3_t &origin) {
    gi.WriteByte(svc_temp_entity);
    gi.WriteByte(TE_PLAIN_EXPLOSION);
    gi.WritePosition(origin);
    
    // Use PHS for explosions so sound carries further
    gi.multicast(origin, MULTICAST_PHS);
}

Spark Effects

void Create_Sparks(const vec3_t &origin, const vec3_t &direction, 
                   spark_type_t type) {
    gi.WriteByte(svc_temp_entity);
    
    switch (type) {
        case SPARK_GENERIC:
            gi.WriteByte(TE_SPARKS);
            break;
        case SPARK_ELECTRIC:
            gi.WriteByte(TE_ELECTRIC_SPARKS);
            break;
        case SPARK_LASER:
            gi.WriteByte(TE_LASER_SPARKS);
            gi.WritePosition(origin);
            gi.WriteDir(direction);
            gi.WriteByte(0xd0);  // Green color
            gi.multicast(origin, MULTICAST_PVS);
            return;
    }
    
    gi.WritePosition(origin);
    gi.WriteDir(direction);
    gi.multicast(origin, MULTICAST_PVS);
}

Steam Effect

void Create_Steam_Puff(const vec3_t &origin, const vec3_t &direction) {
    gi.WriteByte(svc_temp_entity);
    gi.WriteByte(TE_STEAM);
    gi.WritePosition(origin);
    gi.WriteDir(direction);
    
    // Steam parameters
    gi.WriteByte(0xe0);     // Color (white)
    gi.WriteShort(20);      // Particle count
    gi.WriteByte(8);        // Magnitude/intensity
    
    gi.multicast(origin, MULTICAST_PVS);
}

Bubble Trail

void Create_Bubble_Trail(const vec3_t &start, const vec3_t &end) {
    gi.WriteByte(svc_temp_entity);
    gi.WriteByte(TE_BUBBLETRAIL);
    gi.WritePosition(start);
    gi.WritePosition(end);
    gi.multicast(start, MULTICAST_PVS);
}

Teleport Effect

void Spawn_Teleport_Effect(const vec3_t &origin) {
    gi.WriteByte(svc_temp_entity);
    gi.WriteByte(TE_TELEPORT_EFFECT);
    gi.WritePosition(origin);
    gi.multicast(origin, MULTICAST_PVS);
}

Advanced Techniques

Conditional Effects Based on Game Settings

void Spawn_Blood_Effect(const vec3_t &origin, const vec3_t &dir, int damage) {
    // Check if blood is enabled
    if (!g_blood->value) {
        return;  // Don't spawn blood effects
    }
    
    gi.WriteByte(svc_temp_entity);
    gi.WriteByte(damage > 50 ? TE_MOREBLOOD : TE_BLOOD);
    gi.WritePosition(origin);
    gi.WriteDir(dir);
    gi.multicast(origin, MULTICAST_PVS);
}

Surface-Based Effects

void Weapon_Impact_Effect(const vec3_t &point, const vec3_t &normal,
                          const trace_t &trace) {
    gi.WriteByte(svc_temp_entity);
    
    // Determine surface type from trace
    if (trace.surface && (trace.surface->flags & SURF_METAL)) {
        gi.WriteByte(TE_BULLET_SPARKS);
    } else if (trace.contents & CONTENTS_WATER) {
        gi.WriteByte(TE_SPLASH);
        gi.WriteByte(8);
        gi.WritePosition(point);
        gi.WriteDir(vec3_up);
        gi.WriteByte(SPLASH_BLUE_WATER);
        gi.multicast(point, MULTICAST_PVS);
        return;
    } else {
        gi.WriteByte(TE_GUNSHOT);
    }
    
    gi.WritePosition(point);
    gi.WriteDir(normal);
    gi.multicast(point, MULTICAST_PVS);
}

Batching Multiple Effects

void Shotgun_Impact(const vec3_t &origin, const vec3_t &dir) {
    // Spawn blood
    gi.WriteByte(svc_temp_entity);
    gi.WriteByte(TE_MOREBLOOD);
    gi.WritePosition(origin);
    gi.WriteDir(dir);
    gi.multicast(origin, MULTICAST_PVS);
    
    // Spawn extra blood particles around impact
    for (int i = 0; i < 3; i++) {
        vec3_t offset;
        offset[0] = origin[0] + crandom() * 10;
        offset[1] = origin[1] + crandom() * 10;
        offset[2] = origin[2] + crandom() * 10;
        
        gi.WriteByte(svc_temp_entity);
        gi.WriteByte(TE_BLOOD);
        gi.WritePosition(offset);
        gi.WriteDir(dir);
        gi.multicast(origin, MULTICAST_PVS);
    }
}

Distance-Based Quality

void Distant_Spark_Effect(const vec3_t &origin, const vec3_t &viewer_pos) {
    float distance = VectorDistance(origin, viewer_pos);
    
    // Skip effect if too far away
    if (distance > 2048.0f) {
        return;
    }
    
    gi.WriteByte(svc_temp_entity);
    gi.WriteByte(TE_SPARKS);
    gi.WritePosition(origin);
    gi.WriteDir(vec3_up);
    gi.multicast(origin, MULTICAST_PVS);
}

Helper Functions

Creating Reusable Effect Functions

// Generic temp entity spawner
void SVG_SpawnTempEntity(int te_type, const vec3_t &origin, 
                         const vec3_t &direction = vec3_origin) {
    gi.WriteByte(svc_temp_entity);
    gi.WriteByte(te_type);
    gi.WritePosition(origin);
    
    if (direction != vec3_origin) {
        gi.WriteDir(direction);
    }
    
    gi.multicast(origin, MULTICAST_PVS);
}

// Usage
SVG_SpawnTempEntity(TE_GUNSHOT, impact_point, surface_normal);

Effect Manager Class

class EffectManager {
public:
    static void BulletImpact(const vec3_t &pos, const vec3_t &normal, 
                             surf_type_t surface) {
        int effect = (surface == SURF_METAL) ? TE_BULLET_SPARKS : TE_GUNSHOT;
        SpawnEffect(effect, pos, normal);
    }
    
    static void Blood(const vec3_t &pos, const vec3_t &dir, int damage) {
        int effect = (damage > 50) ? TE_MOREBLOOD : TE_BLOOD;
        SpawnEffect(effect, pos, dir);
    }
    
private:
    static void SpawnEffect(int type, const vec3_t &pos, const vec3_t &dir) {
        gi.WriteByte(svc_temp_entity);
        gi.WriteByte(type);
        gi.WritePosition(pos);
        gi.WriteDir(dir);
        gi.multicast(pos, MULTICAST_PVS);
    }
};

Common Mistakes

❌ Forgetting to Call multicast()

// WRONG: Effect is written but never sent!
void BadExample() {
    gi.WriteByte(svc_temp_entity);
    gi.WriteByte(TE_GUNSHOT);
    gi.WritePosition(origin);
    gi.WriteDir(normal);
    // Missing: gi.multicast(origin, MULTICAST_PVS);
}

❌ Wrong Parameter Order

// WRONG: Parameters in wrong order
gi.WriteByte(svc_temp_entity);
gi.WriteByte(TE_STEAM);
gi.WriteDir(direction);        // Direction before position - WRONG!
gi.WritePosition(origin);
gi.WriteByte(0xe0);

Check the Temp Entity Event Types reference for correct parameter order.

❌ Non-Normalized Direction Vector

// WRONG: Direction not normalized
vec3_t dir = {100.0f, 50.0f, 25.0f};  // Length >> 1.0
gi.WriteDir(dir);  // Will encode incorrectly!

// CORRECT: Normalize first
VectorNormalize(dir);
gi.WriteDir(dir);  // Now correct

❌ Using MULTICAST_ALL for Everything

// WRONG: Expensive and unnecessary
gi.multicast(origin, MULTICAST_ALL);

// CORRECT: Use PVS for most effects
gi.multicast(origin, MULTICAST_PVS);

❌ Spawning Too Many Effects

// WRONG: Can cause network congestion
void BadShrapnel() {
    for (int i = 0; i < 100; i++) {  // 100 temp entities!
        gi.WriteByte(svc_temp_entity);
        gi.WriteByte(TE_SPARKS);
        // ... more writes ...
        gi.multicast(origin, MULTICAST_PVS);
    }
}

// CORRECT: Reasonable effect count
void GoodShrapnel() {
    for (int i = 0; i < 5; i++) {  // 5-10 is usually enough
        gi.WriteByte(svc_temp_entity);
        gi.WriteByte(TE_SPARKS);
        // ... more writes ...
        gi.multicast(origin, MULTICAST_PVS);
    }
}

Performance Guidelines

Temp Entity Budget

Per frame, per visible area:

  • Good: 0-10 temp entities
  • Acceptable: 10-30 temp entities
  • Excessive: 30+ temp entities

When to Throttle

// Throttle rapid-fire temp entities
float last_spark_time = 0;

void Create_Continuous_Sparks(const vec3_t &origin) {
    float current_time = level.time;
    
    // Only spawn every 0.1 seconds (10 Hz)
    if (current_time - last_spark_time < 0.1f) {
        return;
    }
    
    last_spark_time = current_time;
    
    gi.WriteByte(svc_temp_entity);
    gi.WriteByte(TE_SPARKS);
    gi.WritePosition(origin);
    gi.WriteDir(vec3_up);
    gi.multicast(origin, MULTICAST_PVS);
}

Network Efficiency

// GOOD: One multicast per effect
void EfficientEffect() {
    gi.WriteByte(svc_temp_entity);
    gi.WriteByte(TE_GUNSHOT);
    gi.WritePosition(pos);
    gi.WriteDir(normal);
    gi.multicast(pos, MULTICAST_PVS);  // Sends to ~5-15 clients
}

// BAD: Multiple unicasts (don't do this!)
void InefficientEffect() {
    for (each client in visible_clients) {
        gi.unicast(client, false);  // Sends separately to each client!
    }
}

Debugging Temp Entities

Enable Developer Console

developer 1

Console Commands

// Show temp entity spawns
set g_debug_temp_entities 1

// Show PVS/PHS areas
r_drawpvs 1

Debug Trail

void Debug_Projectile_Path(const vec3_t &start, const vec3_t &end) {
    gi.WriteByte(svc_temp_entity);
    gi.WriteByte(TE_DEBUGTRAIL);
    gi.WritePosition(start);
    gi.WritePosition(end);
    gi.multicast(start, MULTICAST_ALL);  // Make sure everyone sees it
}

Related Documentation

Summary

To use temporary entities:

  1. Write message type: gi.WriteByte(svc_temp_entity)
  2. Write event type: gi.WriteByte(TE_GUNSHOT) (or other event)
  3. Write parameters: Position, direction, etc. (event-specific)
  4. Send to clients: gi.multicast(origin, MULTICAST_PVS) (or other multicast type)

Key Points:

  • Always call multicast() after writing the effect
  • Use MULTICAST_PVS for most effects (efficient)
  • Normalize direction vectors before writing
  • Don't spawn too many effects (< 30 per frame per area)
  • Check the Event Types reference for parameter order

Temporary entities are the most efficient way to create short-lived visual effects in Q2RTXPerimental!

Clone this wiki locally