Creating Entities
Creating and spawning entities with New and DispatchSpawn, keyvalues, moving and modelling them, removing them, particles and live grenades.
Creating an entity is two steps: create it by classname, which gives you an object that exists but has not spawned, then spawn it. Between the two you set whatever the entity reads on spawn.
Create and spawn
static CBaseEntity* CBaseEntity::CreateEntityByName(const char* pszClassName);
template<typename T>
static T* CBaseEntity::New(const char* pszClassName);
void CBaseEntity::DispatchSpawn(CEntityKeyValues* pEntityKeyValues = nullptr,
HookChain eChain = HookChain::Run);New<T> is CreateEntityByName with the cast done for you, and every
generated class has a non-template New(classname) returning its own type.
The cast is not checked, so the classname has to be one that really creates a
T. Both return nullptr when the entity could not be created. Scripts call
CBaseEntity.CreateEntityByName (or Source2ModEntities.CreateEntityByName)
and view_as the result to the class they want; it is NULL_ENTITY when the
entity could not be created.
#include "source2mod/schema/entity/classes/CBaseModelEntity.h"
CBaseModelEntity* pModel = CBaseEntity::New<CBaseModelEntity>("prop_dynamic_override");
if (!pModel)
return;
pModel->SetModel("models/props/de_dust/hr_dust/dust_crates/dust_crate_style_01_32x32x32.vmdl");
pModel->DispatchSpawn();
const Vector vecOrigin = pPawn->GetAbsOrigin();
pModel->Teleport(&vecOrigin, nullptr, nullptr);CBaseModelEntity model = view_as<CBaseModelEntity>(CBaseEntity.CreateEntityByName("prop_dynamic_override"));
if (model == NULL_ENTITY)
return;
model.SetModel("models/props/de_dust/hr_dust/dust_crates/dust_crate_style_01_32x32x32.vmdl");
model.DispatchSpawn();
float vecOrigin[3];
pawn.GetAbsOrigin(vecOrigin);
model.Teleport(vecOrigin, NULL_VECTOR, NULL_VECTOR);CREATE_ENTITY(classname) and g_pSource2ModEntities->CreateEntityByName() are
the same call through ISource2ModEntities.
Fields you set before DispatchSpawn are what the entity spawns with;
some entities read their state only on spawn, others keep reading it. The FUNPLAY
plugins set up a point_worldtext completely before spawning it:
CPointWorldText* pText = CBaseEntity::New<CPointWorldText>("point_worldtext");
if (!pText)
return;
V_strncpy(pText->m_messageText, "Hello", 512);
pText->m_messageText.NetworkStateChanged();
pText->m_bEnabled = true;
pText->m_flFontSize = 48.0f;
pText->m_Color = Color(255, 255, 255, 255);
pText->m_bFullbright = true;
pText->m_flWorldUnitsPerPx = 0.01f;
pText->DispatchSpawn();CPointWorldText text = view_as<CPointWorldText>(CBaseEntity.CreateEntityByName("point_worldtext"));
if (text == NULL_ENTITY)
return;
text.Set_m_messageText("Hello"); // writes the field and sends the change
text.m_bEnabled = true;
text.m_flFontSize = 48.0;
int color[4] = { 255, 255, 255, 255 };
text.Set_m_Color(color);
text.m_bFullbright = true;
text.m_flWorldUnitsPerPx = 0.01;
text.DispatchSpawn();Keyvalues
Anything a mapper would set in Hammer can be passed as keyvalues. Allocate a
CEntityKeyValues with new and hand it to DispatchSpawn; do not delete it
yourself — the SDK's own CCSCustomHudLayout::Create passes it the same way.
Scripts pass a StringMap of key to string value instead; DispatchSpawn
copies its entries, so close the map afterwards.
#include "entitykeyvalues.h"
auto* pKeyValues = new CEntityKeyValues();
pKeyValues->SetString("targetname", "my_trigger");
pKeyValues->SetString("model", "models/props/de_dust/hr_dust/dust_crates/dust_crate_style_01_32x32x32.vmdl");
pKeyValues->SetInt("spawnflags", 1);
pKeyValues->SetVector("origin", vecOrigin);
pEntity->DispatchSpawn(pKeyValues);StringMap keyValues = new StringMap();
keyValues.SetString("targetname", "my_trigger");
keyValues.SetString("model", "models/props/de_dust/hr_dust/dust_crates/dust_crate_style_01_32x32x32.vmdl");
keyValues.SetString("spawnflags", "1");
char origin[64];
Source2ModApi.Format(origin, sizeof(origin), "%f %f %f", vecOrigin[0], vecOrigin[1], vecOrigin[2]);
keyValues.SetString("origin", origin);
entity.DispatchSpawn(keyValues);
delete keyValues; // DispatchSpawn copied the entriesCEntityKeyValues also has SetBool, SetFloat, SetColor and SetQAngle.
In a script every value is a string, as in Hammer.
Moving and orienting
void CBaseEntity::Teleport(const Vector* pPosition, const QAngle* pAngles, const Vector* pVelocity,
HookChain eChain = HookChain::Run);Pass nullptr for whatever should stay as it is. This is the right way to move
anything, players included — it goes through the entity's own virtual.
Reading position and motion:
| Helper | Returns |
|---|---|
GetAbsOrigin() | World position |
GetAbsRotation() / GetAngRotation() | World / local rotation |
GetAbsVelocity() | Velocity |
CBaseModelEntity::GetEyePosition() | Origin plus m_vecViewOffset |
CCSPlayerPawn::GetEyeAngles() | Where a player is actually looking |
The setters (SetAbsOrigin, SetAbsRotation, SetAngRotation,
SetAbsVelocity, SetBaseVelocity) write the scene node or velocity fields
directly. For a real move prefer Teleport.
Parenting and following go through inputs, the way maps do it:
pText->AcceptInput("SetParent", pPawn, nullptr, "!activator");
pModel->AcceptInput("FollowEntity", pPawn, pModel, "!activator");text.AcceptInput("SetParent", pawn, NULL_ENTITY, "!activator");
model.AcceptInput("FollowEntity", pawn, model, "!activator");Models, bodygroups, collision
void CBaseModelEntity::SetModel(const char* pszModel, HookChain eChain = HookChain::Run);
void CBaseModelEntity::SetBodyGroup(const char* pszName, int nValue);
CUtlSymbolLarge CBaseModelEntity::GetModelName();A model that no map or stock content loads has to be precached before clients
can show it; that happens in the BuildGameSessionManifest game system event
(see Game systems). SetBodyGroup sends the
SetBodyGroup input with "name,value", as the engine has no direct setter.
Collision (the script includes have no COLLISION_GROUP_* / MOVETYPE_*
constants, so scripts pass the numbers):
pEntity->SetCollisionGroup(COLLISION_GROUP_DEBRIS); // also calls CollisionRulesChanged()
uint8 nGroup = pEntity->GetCollisionGroup();
pEntity->SetMoveType(MOVETYPE_NOCLIP); // sets m_MoveType and m_nActualMoveTypeentity.SetCollisionGroup(5); // COLLISION_GROUP_DEBRIS; also calls CollisionRulesChanged()
int nGroup = entity.GetCollisionGroup();
entity.SetMoveType(7); // MOVETYPE_NOCLIP; sets m_MoveType and m_nActualMoveTypeRemoving
void CBaseEntity::Remove(HookChain eChain = HookChain::Run); // UTIL_RemoveOr with the Kill input — immediately, or delayed through the I/O queue:
pEntity->AcceptInput("Kill");
pEntity->AddEntityIOEvent("Kill", nullptr, nullptr, "", 5.0f); // in five secondsentity.AcceptInput("Kill");
entity.AddEntityIOEvent("Kill", NULL_ENTITY, NULL_ENTITY, "", 5.0); // in five secondsThe delayed form is handy for anything temporary: create it, spawn it, and queue its own removal in the same place.
Particles
For an entity-attached effect, DispatchParticleEffect sends it without
creating an entity:
void CBaseEntity::DispatchParticleEffect(const char* pszParticleName,
ParticleAttachment_t nAttachType = ParticleAttachment_t::PATTACH_ABSORIGIN_FOLLOW,
uint8 nAttachmentPoint = 0,
bool bResetAllParticlesOnEntity = false,
IRecipientFilter* pFilter = nullptr,
HookChain eChain = HookChain::Run);A null filter sends it to everyone. To show it to one player, pass a
CSingleRecipientFilter from source2mod/schema/recipientfilter.h; a script
passes a player mask (int[2], the PlayerMask_* stocks) as recipients:
CSingleRecipientFilter filter(pController->GetPlayerSlot());
pPawn->DispatchParticleEffect("particles/my_effect.vpcf",
ParticleAttachment_t::PATTACH_ABSORIGIN_FOLLOW, 0, false, &filter);int recipients[2];
PlayerMask_Clear(recipients);
PlayerMask_Add(recipients, controller.GetSlot());
pawn.DispatchParticleEffect("particles/my_effect.vpcf", 1, 0, false, recipients); // 1 = PATTACH_ABSORIGIN_FOLLOWFor an effect with its own lifetime and position, create an
info_particle_system (CParticleSystem) instead:
auto* pParticle = CBaseEntity::New<CParticleSystem>("info_particle_system");
if (!pParticle)
return;
// CUtlSymbolLarge keeps the pointer it is given: pass a string that outlives
// the entity, like this literal.
pParticle->m_iszEffectName() = "particles/blood_impact/blood_pool.vpcf";
pParticle->DispatchSpawn();
pParticle->Teleport(&vecOrigin, nullptr, nullptr);
pParticle->AcceptInput("Start");
pParticle->AddEntityIOEvent("Kill", pParticle, nullptr, "", 1.0f);CParticleSystem particle = view_as<CParticleSystem>(CBaseEntity.CreateEntityByName("info_particle_system"));
if (particle == NULL_ENTITY)
return;
// m_iszEffectName has no setter in scripts; the "effect_name" keyvalue
// sets it on spawn.
StringMap keyValues = new StringMap();
keyValues.SetString("effect_name", "particles/blood_impact/blood_pool.vpcf");
particle.DispatchSpawn(keyValues);
delete keyValues;
particle.Teleport(vecOrigin, NULL_VECTOR, NULL_VECTOR);
particle.AcceptInput("Start");
particle.AddEntityIOEvent("Kill", particle, NULL_ENTITY, "", 1.0);Grenades
Each grenade projectile class has EmitGrenade, which spawns a live, thrown
grenade the way the game does when a player throws one:
static CHEGrenadeProjectile* CHEGrenadeProjectile::EmitGrenade(
const Vector& vecPosition, const QAngle& angAngle, const Vector& vecVelocity,
CBaseEntity* pOwner, uint32_t nItemDefIndex, HookChain eChain = HookChain::Run);CFlashbangProjectile, CDecoyProjectile and CMolotovProjectile take the same
arguments. CSmokeGrenadeProjectile::EmitGrenade has an extra int nTeam
before the owner:
static CSmokeGrenadeProjectile* CSmokeGrenadeProjectile::EmitGrenade(
const Vector& vecPosition, const QAngle& angAngle, const Vector& vecVelocity,
int nTeam, CBaseEntity* pOwner, uint32_t nItemDefIndex, HookChain eChain = HookChain::Run);pOwner is who gets credit for the grenade (usually a player's pawn) and
nItemDefIndex is the weapon's item definition index. The velocity is used as
both the linear and the angular velocity, as in SwiftlyS2's implementation this
is ported from.
#include "source2mod/schema/entity/classes/CHEGrenadeProjectile.h"
const Vector vecStart = pPawn->GetEyePosition();
const Vector vecVelocity(0.0f, 0.0f, 300.0f);
CHEGrenadeProjectile::EmitGrenade(vecStart, QAngle(0, 0, 0), vecVelocity, pPawn, 44);float vecStart[3];
pawn.GetEyePosition(vecStart);
float vecVelocity[3] = { 0.0, 0.0, 300.0 };
float angles[3]; // 0 0 0
CHEGrenadeProjectile.EmitGrenade(vecStart, angles, vecVelocity, pawn, 44);HookChain
Every helper above that calls a game function takes a HookChain last. The
default, HookChain::Run, goes through other plugins' hooks on that function
as if the game made the call; HookChain::Bypass goes straight to the
original. See Players for when to use which.
Scripts pass HookChain_Run or HookChain_Bypass; the default,
HookChain_Default, keeps the C++ default.
Schema Fields
Reading and writing entity fields, when a change reaches clients on its own and when you call NetworkStateChanged, embedded objects, pointers, arrays and vectors, and extending the classes.
Players
The everyday player helpers — respawning, teams, killing and damaging, items and weapons, attributes, messages, kicking — and the HookChain argument they take.