Raycasting and Queries

Perform spatial queries with raycasting, shape casting, and overlap tests. Learn about collision filtering, hit detection, and query optimization.

Raycasting and spatial queries allow you to detect objects along a line or within a volume. Web Engine supports ray casting (infinite line), shape casting (swept volume), and overlap queries(volume intersection). These are essential for line-of-sight checks, ground detection, hit detection, and AI perception.

Query Types#

Ray Cast

Cast an infinite ray and get the first hit. Used for line-of-sight, shooting, and ground detection.

Shape Cast

Sweep a shape along a direction. Used for character movement prediction and thick raycasts.

Overlap Query

Test if shapes intersect. Used for area detection, explosion radius, and proximity checks.

Collision Filtering

Filter queries by collision groups. Only test specific layers or object types.

Ray Casting#

Ray casting shoots an infinite ray from an origin in a direction and returns the first hit. It's the most common spatial query for hit detection and line-of-sight.

Basic Ray Cast#

// Cast a ray from entity position forward
export default (api) => {
return async (dt) => {
const origin = api.position;
const direction = { x: 0, y: 0, z: 1 }; // Forward
const maxDistance = 10.0;
const hit = await api.raycast(origin, direction, maxDistance);
if (hit) {
api.log(`Hit entity ${hit.eid}`);
api.log(`Distance: ${hit.distance}`);
api.log(`Point: ${hit.point.x}, ${hit.point.y}, ${hit.point.z}`);
api.log(`Normal: ${hit.normal.x}, ${hit.normal.y}, ${hit.normal.z}`);
} else {
api.log("No hit");
}
};
};

Hit Result#

Raycast returns a RaycastHit object with information about the collision:

PropertyTypeDescription
eidnumberEntity ID of the hit object
distancenumberDistance from ray origin to hit point
pointvec3World position of hit point
normalvec3Surface normal at hit point (perpendicular to surface)
interface RaycastHit {
eid: number;
distance: number;
point: [number, number, number];
normal: [number, number, number];
}

Common Raycast Patterns#

Ground Detection#

// Detect ground below character
export default (api) => {
return async (dt) => {
const origin = api.position;
const down = { x: 0, y: -1, z: 0 };
const maxDist = 2.0;
const hit = await api.raycast(origin, down, maxDist);
if (hit && hit.distance < 1.1) {
api.log("On ground");
api.isGrounded = true;
} else {
api.log("In air");
api.isGrounded = false;
}
};
};

Line of Sight#

// Check if enemy has line of sight to player
export default (api) => {
const playerEntity = api.getEntityByName("Player");
return async (dt) => {
const origin = api.position;
// Direction to player
const toPlayer = {
x: playerEntity.position.x - origin.x,
y: playerEntity.position.y - origin.y,
z: playerEntity.position.z - origin.z,
};
// Normalize direction
const length = Math.sqrt(
toPlayer.x * toPlayer.x +
toPlayer.y * toPlayer.y +
toPlayer.z * toPlayer.z
);
const direction = {
x: toPlayer.x / length,
y: toPlayer.y / length,
z: toPlayer.z / length,
};
const hit = await api.raycast(origin, direction, length);
if (hit && hit.eid === playerEntity.id) {
api.log("Player visible!");
// Start attacking
} else {
api.log("Player blocked by obstacle");
// Obstacle in the way
}
};
};

Shooting / Hit Detection#

// Raycast weapon shooting
export default (api) => {
const damage = 25;
const range = 100.0;
return async (dt) => {
if (api.input.fire) {
// Get camera forward direction
const forward = api.camera.forward;
const origin = api.camera.position;
const hit = await api.raycast(origin, forward, range);
if (hit) {
api.log(`Hit ${hit.eid} at distance ${hit.distance}`);
// Apply damage to hit entity
api.sendMessage(hit.eid, {
type: "damage",
amount: damage,
point: hit.point,
normal: hit.normal,
});
// Spawn hit effect at impact point
api.spawnPrefab("BulletImpact", hit.point);
}
}
};
};

Reflecting Rays (Bouncing)#

// Reflect ray off surface normal
export default (api) => {
return async (dt) => {
let origin = api.position;
let direction = { x: 1, y: -1, z: 0 }; // Diagonal
const bounces = 3;
for (let i = 0; i < bounces; i++) {
const hit = await api.raycast(origin, direction, 10.0);
if (hit) {
// Draw debug line
api.drawLine(origin, hit.point, "red");
// Reflect direction around normal
const dot = 2 * (
direction.x * hit.normal.x +
direction.y * hit.normal.y +
direction.z * hit.normal.z
);
direction = {
x: direction.x - dot * hit.normal.x,
y: direction.y - dot * hit.normal.y,
z: direction.z - dot * hit.normal.z,
};
// Continue from hit point
origin = hit.point;
} else {
break;
}
}
};
};

Shape Casting#

Shape casting sweeps a 3D shape along a direction, returning the first hit. It's like a "thick raycast" and is useful for character movement prediction and collision avoidance.

// Shape cast with a sphere
export default (api) => {
return async (dt) => {
const shapeType = 1; // Sphere
const size = [0.5, 0, 0]; // Radius 0.5m
const origin = api.position;
const direction = { x: 0, y: 0, z: 1 };
const maxDistance = 5.0;
const hit = await api.shapecast(
shapeType,
size,
origin,
direction,
maxDistance
);
if (hit) {
api.log(`Shape hit at distance ${hit.distance}`);
}
};
};
Shape TypeSize ParametersUse Case
0 (Box)[halfX, halfY, halfZ]Character sweep, rectangular volumes
1 (Sphere)[radius, 0, 0]Projectile prediction, circular sweep
2 (Capsule)[radius, halfHeight, 0]Character sweep, cylindrical volumes

Character Movement Prediction#

// Predict if movement will collide
export default (api) => {
const characterRadius = 0.5;
const characterHeight = 2.0;
return async (dt) => {
const { move } = api.input;
// Calculate desired movement
const moveDir = {
x: move.x * 5.0 * dt,
y: 0,
z: move.y * 5.0 * dt,
};
// Shape cast capsule in movement direction
const hit = await api.shapecast(
2, // Capsule
[characterRadius, characterHeight / 2, 0],
api.position,
moveDir,
1.0
);
if (hit) {
api.log("Movement blocked - slide along wall");
// Implement sliding logic
} else {
// Safe to move
api.position = {
x: api.position.x + moveDir.x,
y: api.position.y + moveDir.y,
z: api.position.z + moveDir.z,
};
}
};
};

Collision Filtering#

Filter raycasts and queries by collision groups to test only specific object types. This improves performance and prevents unwanted hits.

import { CollisionGroups } from '@web-engine/core';
// Raycast that only hits enemies
const hit = await api.raycast(
origin,
direction,
maxDistance,
{
filterGroups: CollisionGroups.ENEMY
}
);
// Raycast that hits enemies and environment
const hit = await api.raycast(
origin,
direction,
maxDistance,
{
filterGroups: CollisionGroups.ENEMY | CollisionGroups.ENVIRONMENT
}
);
// Raycast that ignores player (for AI line of sight)
const hit = await api.raycast(
origin,
direction,
maxDistance,
{
filterGroups: CollisionGroups.ALL & ~CollisionGroups.PLAYER
}
);

Performance Optimization

Always use collision filtering when you know what you're looking for. Testing only the ENEMY group is much faster than testing all objects and filtering in script.

Overlap Queries#

Overlap queries test if a volume intersects with colliders. Useful for explosion radius, area-of-effect detection, and proximity checks.

// Detect entities in explosion radius
export default (api) => {
const explosionRadius = 5.0;
const explosionDamage = 100;
return (dt) => {
if (api.input.detonate) {
// Get all entities overlapping sphere
const overlapping = api.overlapSphere(
api.position,
explosionRadius
);
for (const eid of overlapping) {
// Calculate damage based on distance
const entity = api.getEntity(eid);
const dx = entity.position.x - api.position.x;
const dy = entity.position.y - api.position.y;
const dz = entity.position.z - api.position.z;
const distance = Math.sqrt(dx*dx + dy*dy + dz*dz);
const damage = explosionDamage * (1 - distance / explosionRadius);
// Apply damage
api.sendMessage(eid, {
type: "damage",
amount: damage,
});
}
// Spawn explosion effect
api.spawnPrefab("Explosion", api.position);
}
};
};

Performance Optimization#

Limit Ray Distance#

Always specify a maximum distance. Infinite rays are expensive.

// Bad: Infinite ray (expensive)
const hit = await api.raycast(origin, direction);
// Good: Limited distance
const hit = await api.raycast(origin, direction, 10.0);

Use Collision Filtering#

// Bad: Test all objects, filter in script
const hit = await api.raycast(origin, direction, 100.0);
if (hit && hit.collisionGroup === CollisionGroups.ENEMY) {
// Handle hit
}
// Good: Filter at physics level
const hit = await api.raycast(
origin,
direction,
100.0,
{ filterGroups: CollisionGroups.ENEMY }
);

Batch Raycasts#

If you need multiple raycasts, batch them together to reduce overhead.

// Batch multiple raycasts (more efficient)
const results = await api.batchRaycast([
{ origin: pos1, direction: dir1, maxDistance: 10 },
{ origin: pos2, direction: dir2, maxDistance: 10 },
{ origin: pos3, direction: dir3, maxDistance: 10 },
]);

Reduce Query Frequency#

// Don't raycast every frame if not needed
export default (api) => {
let timer = 0;
const checkInterval = 0.2; // Check every 200ms
return async (dt) => {
timer += dt;
if (timer >= checkInterval) {
timer = 0;
// Perform expensive raycast
const hit = await api.raycast(origin, direction, 50.0);
// ...
}
};
};

Best Practices#

  • Always specify maxDistance to avoid infinite rays
  • Use collision filtering to test only relevant objects
  • Cache raycast results - don't cast the same ray every frame
  • Use shape casts instead of raycasts for thick objects
  • Batch multiple raycasts when possible
  • Consider raycast frequency - not everything needs frame-perfect accuracy
  • Use raycastSync for immediate results in hot paths
  • Visualize rays with debug drawing during development

Async vs Sync

raycast() is async (returns Promise) and may take a frame.raycastSync() returns immediately but can block the frame. Use async for most cases, sync only when you need instant results.

Debug Visualization#

// Visualize raycasts with debug lines
export default (api) => {
return async (dt) => {
const origin = api.position;
const direction = { x: 0, y: 0, z: 1 };
const maxDistance = 10.0;
const hit = await api.raycast(origin, direction, maxDistance);
if (hit) {
// Draw red line to hit point
api.drawLine(origin, hit.point, "red");
// Draw green sphere at hit point
api.drawSphere(hit.point, 0.1, "green");
// Draw normal arrow
const normalEnd = {
x: hit.point.x + hit.normal.x,
y: hit.point.y + hit.normal.y,
z: hit.point.z + hit.normal.z,
};
api.drawLine(hit.point, normalEnd, "blue");
} else {
// Draw gray line to max distance
const endPoint = {
x: origin.x + direction.x * maxDistance,
y: origin.y + direction.y * maxDistance,
z: origin.z + direction.z * maxDistance,
};
api.drawLine(origin, endPoint, "gray");
}
};
};

Common Pitfalls#

ProblemCauseSolution
Ray not hittingDirection not normalizedNormalize direction vector
Hitting selfRay starts inside colliderOffset origin slightly
Performance issuesToo many raycasts per frameBatch or reduce frequency
Wrong hitsNo collision filteringUse filterGroups parameter
Unreliable ground detectionRay too shortIncrease maxDistance
Physics | Web Engine Docs | Web Engine Docs