Web Engine Docs
Preparing documentation
Use the search bar to quickly find any topic
Preparing documentation
Use the search bar to quickly find any topic
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.
Cast an infinite ray and get the first hit. Used for line-of-sight, shooting, and ground detection.
Sweep a shape along a direction. Used for character movement prediction and thick raycasts.
Test if shapes intersect. Used for area detection, explosion radius, and proximity checks.
Filter queries by collision groups. Only test specific layers or object types.
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.
// Cast a ray from entity position forwardexport default (api) => {return async (dt) => {const origin = api.position;const direction = { x: 0, y: 0, z: 1 }; // Forwardconst 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");}};};
Raycast returns a RaycastHit object with information about the collision:
| Property | Type | Description |
|---|---|---|
| eid | number | Entity ID of the hit object |
| distance | number | Distance from ray origin to hit point |
| point | vec3 | World position of hit point |
| normal | vec3 | Surface normal at hit point (perpendicular to surface) |
interface RaycastHit {eid: number;distance: number;point: { x: number; y: number; z: number };normal: { x: number; y: number; z: number };}
// Detect ground below characterexport 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;}};};
// Check if enemy has line of sight to playerexport default (api) => {const playerEntity = api.getEntityByName("Player");return async (dt) => {const origin = api.position;// Direction to playerconst toPlayer = {x: playerEntity.position.x - origin.x,y: playerEntity.position.y - origin.y,z: playerEntity.position.z - origin.z,};// Normalize directionconst 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}};};
// Raycast weapon shootingexport default (api) => {const damage = 25;const range = 100.0;return async (dt) => {if (api.input.fire) {// Get camera forward directionconst 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 entityapi.sendMessage(hit.eid, {type: "damage",amount: damage,point: hit.point,normal: hit.normal,});// Spawn hit effect at impact pointapi.spawnPrefab("BulletImpact", hit.point);}}};};
// Reflect ray off surface normalexport default (api) => {return async (dt) => {let origin = api.position;let direction = { x: 1, y: -1, z: 0 }; // Diagonalconst bounces = 3;for (let i = 0; i < bounces; i++) {const hit = await api.raycast(origin, direction, 10.0);if (hit) {// Draw debug lineapi.drawLine(origin, hit.point, "red");// Reflect direction around normalconst 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 pointorigin = hit.point;} else {break;}}};};
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 sphereexport default (api) => {return async (dt) => {const shapeType = 1; // Sphereconst size = [0.5, 0, 0]; // Radius 0.5mconst 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 Type | Size Parameters | Use 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 |
// Predict if movement will collideexport default (api) => {const characterRadius = 0.5;const characterHeight = 2.0;return async (dt) => {const { move } = api.input;// Calculate desired movementconst moveDir = {x: move.x * 5.0 * dt,y: 0,z: move.y * 5.0 * dt,};// Shape cast capsule in movement directionconst 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 moveapi.position = {x: api.position.x + moveDir.x,y: api.position.y + moveDir.y,z: api.position.z + moveDir.z,};}};};
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-dev/core';// Raycast that only hits enemiesconst hit = await api.raycast(origin,direction,maxDistance,{filterGroups: CollisionGroups.ENEMY});// Raycast that hits enemies and environmentconst 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 test if a volume intersects with colliders. Useful for explosion radius, area-of-effect detection, and proximity checks.
// Detect entities in explosion radiusexport default (api) => {const explosionRadius = 5.0;const explosionDamage = 100;return (dt) => {if (api.input.detonate) {// Get all entities overlapping sphereconst overlapping = api.overlapSphere(api.position,explosionRadius);for (const eid of overlapping) {// Calculate damage based on distanceconst 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 damageapi.sendMessage(eid, {type: "damage",amount: damage,});}// Spawn explosion effectapi.spawnPrefab("Explosion", api.position);}};};
Always specify a maximum distance. Infinite rays are expensive.
// Bad: Infinite ray (expensive)const hit = await api.raycast(origin, direction);// Good: Limited distanceconst hit = await api.raycast(origin, direction, 10.0);
// Bad: Test all objects, filter in scriptconst hit = await api.raycast(origin, direction, 100.0);if (hit && hit.collisionGroup === CollisionGroups.ENEMY) {// Handle hit}// Good: Filter at physics levelconst hit = await api.raycast(origin,direction,100.0,{ filterGroups: CollisionGroups.ENEMY });
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 },]);
// Don't raycast every frame if not neededexport default (api) => {let timer = 0;const checkInterval = 0.2; // Check every 200msreturn async (dt) => {timer += dt;if (timer >= checkInterval) {timer = 0;// Perform expensive raycastconst hit = await api.raycast(origin, direction, 50.0);// ...}};};
raycastSync for immediate results in hot pathsAsync 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.
// Visualize raycasts with debug linesexport 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 pointapi.drawLine(origin, hit.point, "red");// Draw green sphere at hit pointapi.drawSphere(hit.point, 0.1, "green");// Draw normal arrowconst 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 distanceconst endPoint = {x: origin.x + direction.x * maxDistance,y: origin.y + direction.y * maxDistance,z: origin.z + direction.z * maxDistance,};api.drawLine(origin, endPoint, "gray");}};};
| Problem | Cause | Solution |
|---|---|---|
| Ray not hitting | Direction not normalized | Normalize direction vector |
| Hitting self | Ray starts inside collider | Offset origin slightly |
| Performance issues | Too many raycasts per frame | Batch or reduce frequency |
| Wrong hits | No collision filtering | Use filterGroups parameter |
| Unreliable ground detection | Ray too short | Increase maxDistance |