-
Notifications
You must be signed in to change notification settings - Fork 3
Entity Lifecycle
Understanding the entity lifecycle is crucial for creating custom entities in Q2RTXPerimental. This guide explains how entities are spawned, updated, and destroyed.
Map Load → Spawn → Think Loop → Death → Removal
When: During map load, before entity spawning
What Happens:
- Engine parses
.bspfile entity lump - Creates
cm_entity_tdictionary for each entity - Dictionary contains key-value pairs from map editor
Example Dictionary:
{
"classname" "monster_soldier"
"origin" "128 256 64"
"angle" "90"
"spawnflags" "1"
"target" "target_1"
}
Code:
// Engine creates entity dictionary during map load
cm_entity_t *entities = CM_EntityDictionaries();
for (int i = 0; i < num_entities; i++) {
cm_entity_t *ed = &entities[i];
// Dictionary ready for spawning
}When: During SVG_InitEntities() or dynamic spawning
What Happens:
- Server allocates entity slot from pool
- Calls C++ constructor
- Assigns entity number
- Stores entity dictionary reference
Code:
// Allocate entity of specific type
auto *monster = SVG_Spawn<svg_monster_soldier_t>();
monster->s.number = entity_index;
monster->entityDictionary = ed; // Store dictionaryWhen: Immediately after allocation
Phases:
- PreSpawn: Setup phase
- Spawn: Main initialization (virtual method)
- PostSpawn: Finalization phase
void svg_base_edict_t::DispatchPreSpawnCallback() {
// Called before Spawn()
// Setup basic properties
if (PreSpawn) {
PreSpawn(this);
}
}Typical PreSpawn Actions:
- Parse entity dictionary keys
- Set default values
- Initialize member variables
void svg_monster_soldier_t::Spawn() {
// Main spawning logic
// Set model
gi.SetModel(edict, "models/monsters/soldier/tris.md2");
// Set bounding box
mins = {-16, -16, -24};
maxs = {16, 16, 32};
// Set physics properties
movetype = MOVETYPE_STEP;
solid = SOLID_BBOX;
// Set health
health = 50;
max_health = 50;
// Set callbacks
SetThinkCallback(&svg_monster_soldier_t::AI_Think);
SetTouchCallback(&svg_monster_soldier_t::Touch);
SetDieCallback(&svg_monster_soldier_t::Die);
// Link into world
gi.linkentity(edict);
// Schedule first think
nextthink = level.time + FRAMETIME;
}Typical Spawn Actions:
- Load model/skin
- Set bounding box
- Configure physics (movetype, solid)
- Initialize health/armor
- Set callbacks (Think, Touch, Use, Die, etc.)
- Link entity into world
- Schedule first think
void svg_base_edict_t::DispatchPostSpawnCallback() {
// Called after Spawn()
// Finalization
if (PostSpawn) {
PostSpawn(this);
}
}Typical PostSpawn Actions:
- Find target entities
- Establish entity relationships
- Verify spawn conditions
- Remove if spawn fails
When: Every frame while nextthink <= level.time
Frequency: Typically 40 Hz (every 0.025 seconds)
Flow:
PreThink → Think → PostThink → (repeat)
void svg_base_edict_t::DispatchPreThinkCallback() {
// Before main think
if (PreThink) {
PreThink(this);
}
}Use Cases:
- Player input processing
- Prediction preparation
void svg_monster_soldier_t::Think() {
// Main AI logic
// Check if alive
if (health <= 0) {
return; // Dead, no thinking
}
// AI decision making
if (enemy && CanSeeEnemy()) {
AttackEnemy();
} else {
PatrolArea();
}
// Schedule next think
nextthink = level.time + FRAMETIME;
}Typical Think Actions:
- AI decision making
- Physics simulation
- Animation updates
- Attack/defense logic
- State machine updates
- Schedule next think
void svg_base_edict_t::DispatchPostThinkCallback() {
// After main think
if (PostThink) {
PostThink(this);
}
}Use Cases:
- Finalize player state
- Update view position
- Apply final adjustments
These callbacks can occur at any time during the entity's life:
When: Entity collides with another entity
void svg_trigger_multiple_t::Touch(svg_base_edict_t *other,
const cm_plane_t *plane,
cm_surface_t *surf) {
// Check if player touched trigger
if (!other->client) {
return; // Not a player
}
// Check cooldown
if (timestamp > level.time) {
return; // Too soon
}
// Trigger activation
UseTargets(other, other);
// Set cooldown
timestamp = level.time + wait;
}When: Player presses use key on entity
void svg_func_button_t::Use(svg_base_edict_t *other,
svg_base_edict_t *activator,
entity_usetarget_type_t useType,
int32_t useValue) {
// Button activation
if (health > 0 && !activator->client) {
return; // Only players can use
}
// Play button sound
gi.sound(edict, CHAN_VOICE, moveinfo.sound_start, 1, ATTN_NORM, 0);
// Move button
Button_Move();
// Fire targets
UseTargets(activator, activator);
}When: Entity takes damage (but doesn't die)
void svg_monster_soldier_t::Pain(svg_base_edict_t *other,
float kick,
int32_t damage,
entity_damageflags_t damageFlags) {
// Play pain sound
gi.sound(edict, CHAN_VOICE, sound_pain, 1, ATTN_NORM, 0);
// Play pain animation
SetAnimation(ANIM_PAIN);
// If not alerted, become alerted
if (!enemy) {
enemy = other;
}
}When: Moving entity is blocked by another entity
void svg_func_door_t::Blocked(svg_base_edict_t *other) {
// Damage blocker
if (dmg) {
T_Damage(other, this, this, vec3_zero(), other->s.origin,
vec3_zero(), dmg, 1, 0, MEANS_OF_DEATH_CRUSHED);
}
// Reverse door direction
if (moveinfo.state == STATE_DOWN) {
Door_GoUp(moveinfo.start_origin);
} else {
Door_GoDown();
}
}When: Entity's health drops to 0 or below
Process:
Damage → Health <= 0 → Die Callback → Cleanup
void svg_monster_soldier_t::Die(svg_base_edict_t *inflictor,
svg_base_edict_t *attacker,
int32_t damage,
Vector3 *point) {
// Already dead?
if (health <= GIB_HEALTH) {
// Gib the corpse
ThrowGibs(damage, GIB_ORGANIC);
return;
}
// Play death sound
gi.sound(edict, CHAN_VOICE, sound_death, 1, ATTN_NORM, 0);
// Change entity type to corpse
s.entityType = ET_MONSTER_CORPSE;
// Play death animation
SetAnimation(ANIM_DEATH);
// Stop thinking
SetThinkCallback(nullptr);
nextthink = 0;
// Can no longer take damage
takedamage = DAMAGE_NO;
// Can walk through
solid = SOLID_NOT;
// Drop to floor
movetype = MOVETYPE_TOSS;
// Link changes
gi.linkentity(edict);
}Typical Death Actions:
- Play death sound
- Play death animation
- Change entity type (to corpse)
- Stop thinking
- Disable damage
- Change physics (become non-solid, toss physics)
- Spawn gibs (if extreme damage)
- Award points to killer
When: Entity is no longer needed
Methods:
void Remove_Entity() {
SVG_FreeEdict(entity); // Immediately free
}Use Cases:
- Trigger_once after activation
- Projectiles after impact
- Temporary spawned entities
void svg_monster_soldier_t::Die(/*...*/) {
// ... death logic ...
// Remove corpse after 30 seconds
SetThinkCallback(&svg_base_edict_t::SVG_FreeEdict_Think);
nextthink = level.time + 30.0f;
}Use Cases:
- Corpses (fade out after time)
- Temporary items
- Timed effects
void Think() {
// Check if off-screen and old
if (level.time - spawn_time > 60.0f && !IsVisible()) {
SVG_FreeEdict(this);
return;
}
nextthink = level.time + 1.0f;
}┌─────────────┐
│ Map Loading │
└──────┬──────┘
│
v
┌─────────────────┐
│ Allocate Entity │
└────────┬────────┘
│
v
┌────────────────┐
│ PreSpawn() │
└────────┬───────┘
│
v
┌────────────────┐
│ Spawn() │ ← Main initialization
└────────┬───────┘
│
v
┌────────────────┐
│ PostSpawn() │
└────────┬───────┘
│
v
┌────────────────┐
│ Think Loop │ ← Active gameplay
│ ┌────────────┐ │
│ │PreThink() │ │
│ │Think() │ │◄──── Repeats every frame
│ │PostThink() │ │ (while nextthink set)
│ └────────────┘ │
└────┬───────────┘
│
│ (callbacks during life)
│
v
┌────────────────────┐
│ Touch/Use/Pain/... │ ← Interaction events
└────────┬───────────┘
│
v
┌─────────────┐
│ health <= 0?│
└──────┬──────┘
│ yes
v
┌─────────────┐
│ Die() │ ← Death handler
└──────┬──────┘
│
v
┌─────────────┐
│ Remove │ ← Cleanup
└─────────────┘
void svg_custom_entity_t::Spawn() {
// ALWAYS call parent Spawn() first
svg_base_edict_t::Spawn();
// Set required fields
classname = "custom_entity";
health = 100;
// ALWAYS link after changing position/size
gi.linkentity(edict);
// ALWAYS set nextthink if entity needs to think
nextthink = level.time + FRAMETIME;
}void Think() {
// Check validity
if (!inuse || health <= 0) {
return;
}
// Do work...
// ALWAYS reschedule if thinking should continue
nextthink = level.time + FRAMETIME;
}void Die(/*...*/) {
// Stop future thinks
SetThinkCallback(nullptr);
nextthink = 0;
// Disable damage
takedamage = DAMAGE_NO;
// Change physics
solid = SOLID_NOT;
movetype = MOVETYPE_NONE;
// Update visuals
s.entityType = ET_CORPSE;
// Link changes
gi.linkentity(edict);
}Forgetting to link:
// WRONG: Position changed but not linked
s.origin = new_position;
// Clients won't see the change!
// CORRECT:
s.origin = new_position;
gi.linkentity(edict);Not rescheduling think:
// WRONG: Think() called once then never again
void Think() {
DoWork();
// Missing: nextthink = level.time + FRAMETIME;
}Accessing freed entity:
// WRONG: Using entity after freeing
SVG_FreeEdict(target);
target->health = 0; // CRASH! Entity is freed!- Entity System Overview - Entity architecture
- Entity Base Class Reference - svg_base_edict_t details
- Creating Custom Entities - Step-by-step tutorial
- Entity Networking - How entity state is synchronized
- Server Game Module - Server-side systems
The entity lifecycle in Q2RTXPerimental follows this flow:
- Dictionary Creation: Map load creates key-value pairs
- Allocation: Entity slot allocated from pool
- Spawning: PreSpawn → Spawn → PostSpawn
- Think Loop: PreThink → Think → PostThink (repeats)
- Interaction: Touch, Use, Pain, Blocked callbacks
- Death: Die callback when health <= 0
- Removal: Immediate or delayed cleanup
Key Points:
- Always call parent Spawn() first
- Always link after position/size changes
- Always reschedule nextthink if thinking should continue
- Stop thinking and disable damage in Die()
- Never access freed entities
Understanding this lifecycle is essential for creating robust custom entities that behave correctly throughout their existence in the game world.