Tracing
Line and hull traces against the world and entities — what a player is looking at, line of sight, and whether a box fits.
ISource2ModTrace runs physics traces through the game's own trace function:
rays along a view direction, rays between two points, hull (box) sweeps, and a
low-level call for your own filter and ray shape. Results come back as a
TraceResult.
#include "source2mod/ISource2ModTrace.h"#include <source2mod/api>The calls
TraceResult TraceShape(const Vector& vecStart, const QAngle& angAngles, CBaseEntity* pIgnoreEntity, TraceOptions* pTraceOptions);
TraceResult TraceEndShape(const Vector& vecStart, const Vector& vecEnd, CBaseEntity* pIgnoreEntity, TraceOptions* pTraceOptions);
TraceResult TraceHullShape(const Vector& vecStart, const Vector& vecEnd, const Vector& vecMins, const Vector& vecMaxs,
CBaseEntity* pIgnoreEntity, TraceOptions* pTraceOptions);
TraceResult TraceShapeEx(const Vector& vecStart, const Vector& vecEnd, CTraceFilter* pTraceFilter, Ray_t* pRay);Each has a macro: TRACE_SHAPE, TRACE_END_SHAPE, TRACE_HULL_SHAPE,
TRACE_SHAPE_EX (same arguments). TraceShape casts a line 8192 units along
the angles.
A script calls Source2ModTrace.TraceShape(), TraceEndShape(),
TraceHullShape() and TraceShapeEx() with float[3] vectors; the ignored
entity and the options have defaults (NULL_ENTITY, null). TraceOptions
and TraceResult are handles there — delete both when done.
What is the player looking at?
CCSPlayerPawn* pawn = player->GetPlayerPawn();
if (!pawn)
return;
Vector eyes = pawn->GetEyePosition();
QAngle angles = pawn->GetEyeAngles();
TraceOptions opts{};
opts.InteractsExclude = CONTENTS_PLAYER | CONTENTS_PLAYER_CLIP;
TraceResult result = TRACE_SHAPE(eyes, angles, pawn, &opts);
if (result.DidHit())
{
Vector hit = result.EndPos();
auto* target = static_cast<CBaseEntity*>(result.HitEntity()); // may be nullptr (world)
}CCSPlayerPawn pawn = player.GetPlayerPawn();
if (pawn == NULL_ENTITY)
return;
float eyes[3], angles[3];
pawn.GetEyePosition(eyes);
pawn.GetEyeAngles(angles);
// Contents masks are two cells; Mask_Add takes a CONTENTS_*_BIT.
int exclude[2];
Mask_Add(exclude, CONTENTS_PLAYER_BIT);
Mask_Add(exclude, CONTENTS_PLAYER_CLIP_BIT);
TraceOptions opts = new TraceOptions();
opts.SetInteractsExclude(exclude);
TraceResult result = Source2ModTrace.TraceShape(eyes, angles, pawn, opts);
delete opts;
if (result.DidHit())
{
float hit[3];
result.EndPos(hit);
CEntityInstance target = result.HitEntity(); // may be NULL_ENTITY (world)
}
delete result;The ignored entity (pawn here) is filtered out together with its owner and
everything sharing its collision hierarchy, so the player does not hit
themselves or their own weapon.
Line of sight between two points
bool CanSee(CCSPlayerController* player, CCSPlayerController* target)
{
CCSPlayerPawn* from = player->GetPlayerPawn();
CCSPlayerPawn* to = target->GetPlayerPawn();
if (!from || !to)
return false;
TraceOptions opts{};
opts.InteractsExclude = CONTENTS_PLAYER_CLIP;
TraceResult result = TRACE_END_SHAPE(from->GetEyePosition(), to->GetEyePosition(), from, &opts);
return result.HitEntity() == to;
}bool CanSee(CCSPlayerController player, CCSPlayerController target)
{
CCSPlayerPawn from = player.GetPlayerPawn();
CCSPlayerPawn to = target.GetPlayerPawn();
if (from == NULL_ENTITY || to == NULL_ENTITY)
return false;
float start[3], end[3];
from.GetEyePosition(start);
to.GetEyePosition(end);
int exclude[2];
Mask_Add(exclude, CONTENTS_PLAYER_CLIP_BIT);
TraceOptions opts = new TraceOptions();
opts.SetInteractsExclude(exclude);
TraceResult result = Source2ModTrace.TraceEndShape(start, end, from, opts);
bool seen = result.HitEntity() == to;
delete opts;
delete result;
return seen;
}Hull traces
A hull trace sweeps a box from start to end — use it to test whether a player fits somewhere before teleporting them.
Vector mins = pawn->m_pCollision()->m_vecMins();
Vector maxs = pawn->m_pCollision()->m_vecMaxs();
TraceOptions opts{};
opts.InteractsWith = MASK_PLAYERSOLID;
TraceResult result = TRACE_HULL_SHAPE(dest, dest, mins, maxs, pawn, &opts);
if (result.IsAllSolid())
return; // would be stuckfloat mins[3], maxs[3];
CCollisionProperty collision = pawn.m_pCollision;
collision.m_vecMins(mins);
collision.m_vecMaxs(maxs);
int mask[2];
Mask_PlayerSolid(mask);
TraceOptions opts = new TraceOptions();
opts.SetInteractsWith(mask);
TraceResult result = Source2ModTrace.TraceHullShape(dest, dest, mins, maxs, pawn, opts);
bool stuck = result.IsAllSolid();
delete opts;
delete result;
if (stuck)
return; // would be stuckTraceOptions
struct TraceOptions
{
uint64_t InteractsAs = CONTENTS_EMPTY;
uint64_t InteractsWith = MASK_SHOT;
uint64_t InteractsExclude = CONTENTS_EMPTY;
};The three masks are applied to the filter exactly as given.
In a script TraceOptions is a handle: new TraceOptions(), then
SetInteractsAs(), SetInteractsWith(), SetInteractsExclude(). A contents
mask is two cells (int[2]), built with Mask_Add(mask, CONTENTS_*_BIT) or
the stocks Mask_Shot(), Mask_Solid(), Mask_PlayerSolid(),
Mask_ShotHull() and Mask_Water().
nullptr and TraceOptions{} are not the same. With nullptr the
filter interacts with MASK_ALL; a default-constructed TraceOptions
interacts with MASK_SHOT. Pass options explicitly when the mask matters.
In a script the same goes for null and new TraceOptions().
Custom filter and ray: TraceShapeEx
TraceShapeEx takes your own CTraceFilter and Ray_t. CTraceFilterEx is
the ordinary filter — ignore an entity, its owner and its hierarchy — and its
public masks can be changed before the trace. A script passes the same pieces
as arguments: options, an entity to ignore, an optional filter function
(bool (CEntityInstance entity, any data), false lets the ray through) and
the ray shape (RAY_TYPE_LINE, RAY_TYPE_SPHERE, RAY_TYPE_HULL,
RAY_TYPE_CAPSULE, sized by a, b and radius):
CTraceFilterEx filter(pawn);
filter.m_nInteractsWith = MASK_PLAYERSOLID;
Ray_t ray(mins, maxs); // a hull ray; Ray_t() is a line
TraceResult result = TRACE_SHAPE_EX(start, end, &filter, &ray);int mask[2];
Mask_PlayerSolid(mask);
TraceOptions opts = new TraceOptions();
opts.SetInteractsWith(mask);
// Ignore pawn (with its owner and hierarchy), no filter function, a hull ray
// from mins to maxs; RAY_TYPE_LINE (the default) is a line.
TraceResult result = Source2ModTrace.TraceShapeEx(start, end, opts, pawn, INVALID_FUNCTION, 0, RAY_TYPE_HULL, mins, maxs);
delete opts;TraceResult
| Accessor | Meaning |
|---|---|
DidHit() | Fraction() < 1.0f |
Fraction() | How far the trace got, 1.0 = nothing hit |
StartPos(), EndPos() | Where it started and stopped |
HitPoint() | Exact hit point when HasExactHit(), otherwise equal to EndPos() |
Normal() | Surface normal at impact |
HitEntity() | The CEntityInstance* hit, or nullptr |
IsAllSolid() | The start point was inside something solid |
HitboxBoneIndex(), Hitbox() | Hitbox that was hit (with CONTENTS_HITBOX) |
Contents(), Surface() | Contents and surface properties of what was hit |
TraceResult is move-only (copying is deleted). Keep it by value from the
call, or copy out the fields you need.
In a script a TraceResult is a handle to delete. Vectors come out through
a float[3] argument (result.EndPos(pos)), HitEntity() is NULL_ENTITY
when no entity was hit, Hitbox() is HitGroup(), HitboxIndex() and
HitboxName(), and Surface() is SurfaceName() and SurfacePhysics().
Other queries
| Call | Macro |
|---|---|
PointContents(const Vector* const vTestPos, uint64 nContentsMask) | POINT_CONTENTS(pos, mask) |
GetEntityWorldSpaceAABB(const CBaseEntity* const rEntity, Vector* pMinsOut, Vector* pMaxsOut) | ENTITY_WORLD_AABB(ent, mins, maxs) |
CheckAreaOverlappingEntity(const CCSNavArea* const rArea, const CBaseEntity* const rEntity, bool bExtrudeHullHeight) | CHECK_AREA_OVERLAPPING_ENTITY(area, ent, extrudeHull) |
A script calls Source2ModTrace.PointContents(pos, mask, contents) (masks are
int[2]), Source2ModTrace.GetEntityWorldSpaceAABB(entity, mins, maxs) and
Source2ModTrace.CheckAreaOverlappingEntity(area, entity, extrudeHullHeight);
nav areas come from the CCSNavArea methodmap
(CCSNavArea.GetClosestNavArea(pos), CCSNavArea.GetAllNavAreas()).
Pitfalls
- Main thread only. Traces read the live physics world.
- Check
HitEntity()fornullptr(NULL_ENTITYin a script) — hitting the world is a hit without an entity.dynamic_castit to the class you expect (it works on game types), or check the classname andstatic_cast. - A failed lookup returns an empty result. If Source2Mod cannot find the
engine's trace interface (for example after a game update), every trace
returns a default
TraceResult—DidHit()false,Fraction()1.0 — and an error is logged. TraceShapestops at 8192 units; useTraceEndShapefor anything longer.