A cross-platform raycast system for Unity with custom primitive support and spatial acceleration structures. Built with a pure C# core that can run outside Unity environments.
Measured with the included headless harness (.NET 8, Release, 20,000 rays per batch, seeded random scenes, QuadTree enabled). Absolute numbers vary with hardware:
| Scene | Throughput | Narrow-phase tests / ray | GC allocation / raycast |
|---|---|---|---|
| 100 boxes | ~1,190,000 rays/s | < 0.01 | 0 B |
| 2,000 boxes | ~209,000 rays/s | 0.09 | 0 B |
| 2,000 mixed spheres + boxes | ~144,000 rays/s | 0.46 | 0 B |
| 10,000 boxes | ~58,000 rays/s | 0.36 | 0 B |
Engine design behind the numbers:
- Quadtree with cached per-subtree bounds and near-to-far, early-out traversal — closest-hit queries skip whole subtrees that cannot beat the current best hit.
- Zero-allocation query path:
Raycastproduces no garbage at steady state. - Deterministic
RaycastAllordering (distance, then primitive id).
Tests.Core/ is a headless xUnit suite — run dotnet test Tests.Core. It covers a
QuadTree==SimpleList differential (including move/remove mutation scripts), geometric
invariants, capsule geometry, the id contract, and zero-allocation assertions.
- 🌐 Cross-Platform - Pure C# core works in Unity and standalone environments
- 🎯 Custom Primitives - Box, Sphere and Capsule collision detection
- 📊 Dual Acceleration - QuadTree and SimpleList spatial structures
- ♻️ Zero-GC raycasts - closest-hit queries allocate nothing at steady state
- 🔧 Modular Design - Separated Core logic and Unity integration layer
- 🧪 Performance Testing - Built-in comparison tools with Unity Physics
- ⚡ Configurable - Optimizable for different scene sizes
Unlike Unity Physics, this module runs in pure C# environments:
- Dedicated game servers
- Headless simulations
- Non-Unity applications
- Cross-platform game logic
For very small scenes (< 20-30 colliders), disable QuadTree acceleration — a plain list scan is cheaper than tree traversal.
The system is built with two distinct layers:
CustomRaycastSystemCore- Main raycast engineBoxPrimitive,SpherePrimitive&CapsulePrimitive- Collision primitivesQuadTree&SimpleListAccelerationStructure- Spatial optimization- Platform-agnostic math utilities
CustomRaycastSystem- Unity singleton wrapperCustomBoxCollider&CustomSphereCollider- Unity componentsCustomHitInfo- Unity-compatible hit information
- Shape: Oriented bounding box (OBB)
- Properties: Position, Rotation, Size (3D scale)
- Features: Full transform support, non-uniform scaling
- Usage: Perfect for rectangular objects, platforms, walls
- Shape: Perfect sphere
- Properties: Position, Radius
- Features: Uniform scaling only, rotation ignored
- Usage: Ideal for projectiles, characters, circular areas
- Shape: Oriented capsule (cylinder with hemispherical caps, local +Y axis)
- Properties: Position, Rotation, Size = (radius, totalHeight, radius)
- Features: Full transform support; totalHeight is clamped to ≥ 2×radius
- Usage: Characters, pills, rounded obstacles
-
Import the System
Assets/Custom Raycast System/ ├── Core/ # Platform-agnostic raycast engine ├── Unity Layer/ # Unity-specific components └── Tests/ # Demo and testing scripts Tests.Core/ # Headless xUnit test suite (dotnet test Tests.Core) -
Add the Singleton
- Create an empty GameObject in your scene
- Add the
CustomRaycastSystemcomponent - For small scenes: Disable "Use Quad Tree" for better performance
-
Replace Colliders
// Instead of SphereCollider gameObject.AddComponent<CustomSphereCollider>(); // Instead of BoxCollider gameObject.AddComponent<CustomBoxCollider>();
Capsules are Core-level primitives (no Unity component yet) — register manually:
CustomRaycastSystem.Instance.RegisterPrimitive( new CapsulePrimitive(0, position, rotation, new Vector3(radius, height, radius)), gameObject);
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
if (CustomRaycastSystem.Instance.Raycast(ray, out CustomHitInfo hit, 100f))
{
Debug.Log($"Hit: {hit.HitGameObject.name} at {hit.HitPoint}");
Debug.Log($"Distance: {hit.Distance}, Normal: {hit.Normal}");
}Ray ray = new Ray(transform.position, Vector3.forward);
var hits = CustomRaycastSystem.Instance.RaycastAll(ray, 50f, sortByDistance: true);
foreach (var hit in hits)
{
Debug.Log($"Hit object: {hit.HitGameObject.name}");
}// Sphere Collider
var sphereCollider = gameObject.AddComponent<CustomSphereCollider>();
sphereCollider.Radius = 2.5f;
// Box Collider
var boxCollider = gameObject.AddComponent<CustomBoxCollider>();
boxCollider.Size = new Vector3(2f, 1f, 3f);Run the InteractiveRaycastTest to see mouse-based raycasting in action:
- Mouse over objects to highlight them
- Objects move in real-time with smooth animations
- Visual feedback with color changes
Use AutoTest for performance and accuracy comparison:
- Generates random spheres and boxes
- Compares custom system vs Unity Physics
- Reports timing differences and validates accuracy
- Configurable test parameters
QuadTree (For larger scenes with 30+ objects)
[SerializeField] private bool _useQuadTree = true;
[SerializeField] private Vector3 _quadTreeCenter = Vector3.zero;
[SerializeField] private Vector3 _quadTreeSize = new Vector3(1000, 1000, 1000);
[SerializeField] private int _quadTreeCapacity = 4;SimpleList (Recommended for small scenes)
[SerializeField] private bool _useQuadTree = false; // Better for <30 objectsRaycast(Ray ray, out CustomHitInfo hitInfo, float maxDistance)- Single raycastRaycastAll(Ray ray, float maxDistance, bool sortByDistance)- Multiple raycastsRegisterPrimitive(IPrimitive primitive, GameObject gameObject)- Manual registration (returns the primitive with its assigned ID)UnregisterPrimitive(int primitiveId)- Manual removalUpdatePrimitive(int primitiveId, Vector3 position, Quaternion rotation, Vector3 size)- Move / re-orient / resize a registered primitive
IDs are system-owned: constructor ids are placeholders, AddPrimitive/RegisterPrimitive assign unique sequential ids.
GameObject HitGameObject- The hit GameObject (Unity only)Vector3 HitPoint- World space hit positionVector3 Normal- Surface normal at hit pointfloat Distance- Distance from ray originint PrimitiveID- Unique primitive identifier
The core system works independently of Unity for server-side logic:
// Create the core system (useQuadTree, worldCenter, worldSize, nodeCapacity)
var coreSystem = new CustomRaycastSystemCore(false, UVector3.zero,
new UVector3(1000, 1000, 1000), 4);
// Constructor ids are placeholders — AddPrimitive assigns and returns unique ids
var box = coreSystem.AddPrimitive(new BoxPrimitive(0, position, rotation, size));
var sphere = coreSystem.AddPrimitive(new SpherePrimitive(0, position, radius));
var capsule = coreSystem.AddPrimitive(
new CapsulePrimitive(0, position, rotation, new UVector3(radius, height, radius)));
// Raycast
var ray = new CRay(origin, direction, maxDistance);
if (coreSystem.Raycast(ray, out CHitInfo hit, maxDistance))
{
Console.WriteLine($"Hit primitive {hit.PrimitiveID} at distance {hit.Distance}");
}
// Move / re-orient / resize later, by id
coreSystem.UpdatePrimitive(box.ID, newPosition, newRotation, newSize);- ✅ Large scenes (world size > 100 units)
- ✅ Many objects (30+ colliders)
- ✅ Sparse object distribution
- ✅ Long-range raycasts
- ✅ Small scenes (world size < 100 units)
- ✅ Few objects (< 30 colliders)
- ✅ Dense object clusters
- ✅ Short-range raycasts
- Raycasts allocate 0 B at steady state;
RaycastAllallocates only the returned list - Scene storage: ~0.3–0.4 KB per primitive including QuadTree overhead (≈650 KB for 2,000 primitives)
- Unity 2019.4 or later (for Unity layer)
- .NET Standard 2.0 compatible core; no external dependencies
- .NET 8 SDK (optional) — only for running the headless test suite (
Tests.Core/)
- Prototyping physics systems
- Custom collision detection
- Educational purposes
- Cross-platform game development
- Dedicated game servers
- Physics simulations
- Pathfinding systems
- Non-Unity game engines
MIT — see LICENSE. Feel free to use and modify for your projects.
Need help? Check the test scenes for complete working examples, or examine the InteractiveRaycastTest script for usage patterns.
