Traces
Both calls need the internal running and needs_internal = true in script_info.
trace.line(from, to [, {id = 0, mask = 0, skip_local = true}]) -> id | nil
trace.get(id) -> table | niltrace.line registers a line to trace through the game's collision and returns an id. trace.get(id) returns the latest result for that id, or nil until the first result lands, usually a tick or two later. from and to are vec3 or {x=, y=, z=} in world units.
| Option | Default | Meaning |
|---|---|---|
id | 0 | Refresh the request with this id instead of creating a new one |
mask | 0 | Which collision set to trace against |
skip_local | true | Ignore your own pawn |
Masks
mask | Set |
|---|---|
0 | Visibility. Player pawns are not solid, so a line that reaches the far point (fraction 1.0, no entity) means the point is visible |
1 | Bullets. What a shot collides with. Players come back in entity |
2 | Bullets, swept as a 16 unit sphere |
Other values are treated as 0. One request has one mask; for both a visibility and a bullet answer, register two traces.
Result fields
| Field | Meaning |
|---|---|
id | Request id |
entry_seq | Changes each time the line was re-traced |
tick | Tick the line was traced on |
fraction | How far along the line the trace got, 0 to 1 |
hit | True when the line stopped short, hit an entity, or started inside solid |
start_solid | The start point was inside a solid |
x, y, z | End point, world units |
nx, ny, nz | Surface normal at the hit |
contents | Contents bits of the surface hit |
surface_flags | Same value as contents, kept for scripts ported from other loaders |
hitgroup | Hit group, -1 for none |
entity_ptr | 0 for nothing or the world |
entity_class | Designer name of the entity hit, "" for none |
entity | Player object when the entity hit is a player |
On mask 0, hit against world geometry means the line is blocked.
Keeping a trace alive
Pass the id back in opts.id on the next call to refresh the same request with new endpoints. A request not refreshed for 2 seconds is dropped and its id freed. The result table is reused between calls; entry_seq changes when the line was actually re-traced.
Limits
- 32 requests across all scripts. The 33rd returns
niland logs once. - Lines are re-traced within 50 ms, not every tick.
- Ids are never
0.
Returns nil when
trace.line: the flag is missing, an endpoint is not avec3or{x, y, z}table, the internal is not running, or 32 requests are already livetrace.get: the flag is missing, the id is0, the internal is not running, or no result has landed yet
Example
One ray per enemy head, kept alive by its id:
script_info = { needs_internal = true }
local rays = {} -- key -> trace id
function on_tick()
local me = local_player()
if not me or not internal.is_loaded() then return end
local cam = camera.get_position()
for _, p in ipairs(get_players() or {}) do
if p:is_alive() and p:is_enemy() then
local b = p:get_bone_position("head")
if b then
local key = p:entity_ptr() .. ":head"
rays[key] = trace.line(cam, { x = b.x, y = b.y, z = b.z },
{ id = rays[key], mask = 0, skip_local = true })
local r = rays[key] and trace.get(rays[key]) or nil
if r then
local visible = (r.entity ~= nil and r.entity_ptr == p:entity_ptr())
or r.fraction >= 0.97
-- draw or act on `visible`
end
end
end
end
endThis answers the same question as p:is_visible, on your own schedule. p:is_visible and p:is_bone_visible on the Player page need neither the flag nor the internal.