Small helpers that fix everyday math and Unity annoyances: safe modulo, wrapped indices, approximate equality, bounds math, color utilities, and more.
Copy/paste examples and diagrams show intent; use as building blocks in hot paths.
This guide summarizes the math primitives and extension helpers in this package and shows how to apply them effectively, with examples, performance notes, and practical scenarios.
varbulletPath=newLine2D(bulletStart,bulletEnd);varenemy=newCircle(enemyPosition,enemyRadius);if(bulletPath.Intersects(enemy)){// Bullet hit the enemyenemy.TakeDamage(bulletDamage);}
varpath=newLine2D(pathStart,pathEnd);Vector2snappedPosition=path.ClosestPointOnLine(mouseWorldPos);// Use for UI snapping, path following, or grid alignment
Performance tip: Use DistanceSquaredToPoint instead of DistanceToPoint when comparing distances (avoids expensive square root):
usingWallstopStudios.UnityHelpers.Core.Math;varray=newLine3D(gunBarrel.position,hitPoint);varenemyBounds=newBoundingBox3D(enemy.bounds);// Check if ray hits enemy bounding boxif(ray.Intersects(enemyBounds)){enemy.TakeDamage(bulletDamage);}
Closest points between two 3D lines (skew lines):
Problem: In 3D, two lines might not actually intersect (imagine two pipes that pass by each other). This finds the closest approach.
varropeA=newLine3D(ropeAStart,ropeAEnd);varropeB=newLine3D(ropeBStart,ropeBEnd);if(ropeA.TryGetClosestPoints(ropeB,outVector3pointOnA,outVector3pointOnB)){floatseparation=Vector3.Distance(pointOnA,pointOnB);if(separation<0.1f){// Ropes are touching or tangled}}
varlaserBeam=newLine3D(laserStart,laserEnd);varshield=newSphere(shieldCenter,shieldRadius);if(laserBeam.Intersects(shield)){floatdistance=laserBeam.DistanceToSphere(shield);// distance == 0 means line passes through sphere// distance > 0 means line misses sphere}
usingWallstopStudios.UnityHelpers.Core.Math;varp=newParabola(maxHeight:5f,length:10f);if(p.TryGetValueAtNormalized(0.5f,outfloaty)){// y == 5 (at the peak)}
// In a tight loop updating many projectilesfor(inti=0;i<projectiles.Length;i++){floatx=projectiles[i].distanceTraveled;if(x>=0&&x<=parabola.Length){floaty=parabola.GetValueAtUnchecked(x);// No bounds checkprojectiles[i].position.y=y;}}
// Normalized: Use when working with 0-1 interpolation (animations)floatt=animationTime/totalDuration;// 0-1parabola.TryGetValueAtNormalized(t,outfloaty);// Absolute: Use when working with world-space coordinatesfloatworldX=transform.position.x;parabola.TryGetValueAt(worldX,outfloatworldY);
Point-in-Polygon — Test if points are inside shapes¶
Why it exists: Detects whether a point lies inside an irregular polygon, solving the "did the player click this shape" problem.
When to use:
Click detection in irregular UI shapes or game zones
Testing if characters are inside territory boundaries
Checking if waypoints are in walkable areas
Testing if 3D points project inside mesh faces
When NOT to use:
For circles (use Vector2.Distance(point, center) <= radius)
For rectangles (use Rect.Contains)
For complex 3D volumes (use Collider.bounds or raycasts)
Important: This uses the ray-casting algorithm — it counts how many times a ray from the point crosses polygon edges. Odd count = inside, even count = outside.
usingWallstopStudios.UnityHelpers.Core.Math;Vector2[]zoneShape=newVector2[]{new(0,0),new(10,0),new(10,5),new(5,10),new(0,5)};Vector2clickPos=Camera.main.ScreenToWorldPoint(Input.mousePosition);if(PointPolygonCheck.IsPointInsidePolygon(clickPos,zoneShape)){Debug.Log("Clicked inside the zone!");}
// Test if 3D point is inside a 3D triangle (projects onto plane)Vector3[]triangleFace=newVector3[]{new(0,0,0),new(5,0,0),new(2.5f,5,0)};Vector3faceNormal=Vector3.forward;// Must be normalizedVector3testPoint=newVector3(2.5f,2f,1f);// Will project onto z=0 planeif(PointPolygonCheck.IsPointInsidePolygon(testPoint,triangleFace,faceNormal)){Debug.Log("Point projects inside triangle");}
// Use ReadOnlySpan to avoid heap allocationsSpan<Vector2>vertices=stackallocVector2[4]{new(0,0),new(1,0),new(1,1),new(0,1)};boolinside=PointPolygonCheck.IsPointInsidePolygon(clickPos,vertices);// No GC allocations when using ReadOnlySpan
Edge cases to know:
Points exactly on polygon edges may return inconsistent results (floating-point precision issues)
Assumes simple (non-self-intersecting) polygons
Winding order (clockwise vs counter-clockwise) doesn't matter
For 3D: all polygon vertices must be coplanar for accurate results
Polyline simplification (Douglas–Peucker)
Simplify (float epsilon) and SimplifyPrecise (double tolerance) reduce vertex count while preserving shape.
// Split large collections into fixed-size batchesvaritems=Enumerable.Range(0,100);foreach(varbatchinitems.Partition(10)){// Process 10 items at a timeProcessBatch(batch);// batch is a List<int> of size 10}// Zero-allocation version for hot pathsusing(varbatchBuffer=items.PartitionPooled(10)){foreach(varbatchinbatchBuffer){// batch is reused from pool, no allocations}}// Automatically returns buffer to pool
// Fast removal when order doesn't matter (particle systems, entity lists)List<Enemy>enemies=GetActiveEnemies();enemies.RemoveAtSwapBack(3);// Swaps enemy[3] with last enemy, then removes// Avoids O(n) shift operation of List.RemoveAt by swapping with last element
usingWallstopStudios.UnityHelpers.Core.Extension;// Thread-safe for ConcurrentDictionaryvarvalue=dict.GetOrAdd(key,()=>newExpensiveObject());// Read-only version (doesn't modify dict)varvalue=readOnlyDict.GetOrElse(key,defaultValue);
usingWallstopStudios.UnityHelpers.Core.Extension;// Compute BoundsInt for occupied grid cellsVector3Int[]positions=GetOccupiedCells();BoundsInt?area=positions.GetBounds(inclusive:false);if(areaisBoundsIntb){// b contains all positions}
// Merge many Bounds (e.g., from Renderers)Renderer[]renderers=GetComponentsInChildren<Renderer>();Bounds?merged=renderers.Select(r=>r.bounds).GetBounds();if(mergedisBoundstotalBounds){// totalBounds encompasses all renderers}
// Calculate how many edits to transform one string into anotherstringa="kitten";stringb="sitting";intdistance=a.LevenshteinDistance(b);// 3 edits// Use for: fuzzy matching, spell correction, search suggestions
publicenumGameState{MainMenu,Playing,Paused,GameOver}GameStatestate=GameState.Playing;// ❌ SLOW: Uses reflection every timestringname=state.ToString();// ✅ FAST: Cached in array/dictionary after first callstringcached=state.ToCachedName();// Subsequent calls are O(1) lookups with zero allocation
Performance: ToCachedName uses cached lookups to avoid repeated allocations and string conversions after the first call.
usingWallstopStudios.UnityHelpers.Core.Attribute;publicenumDifficulty{[EnumDisplayName("Easy Mode")]Easy,[EnumDisplayName("Normal")]Medium,[EnumDisplayName("NIGHTMARE MODE!!!")]Hard}Difficultycurrent=Difficulty.Hard;stringdisplayName=current.ToDisplayName();// "NIGHTMARE MODE!!!"// Falls back to enum name if attribute not present
usingWallstopStudios.UnityHelpers.Core.Extension;// Items with different drop chancesvarloot=new[]{(item:"Common Sword",weight:50),(item:"Rare Shield",weight:30),(item:"Epic Helmet",weight:15),(item:"Legendary Ring",weight:5)};IRandomrng=PRNG.Instance;stringdrop=rng.NextWeighted(loot);// More likely to get Common Sword// Get index instead of valueintdropIndex=rng.NextWeightedIndex(loot.Select(x=>x.weight));
// Random point in rectangleVector2point=rng.NextVector2(minX,maxX,minY,maxY);// Random point inside circleVector2inCircle=rng.NextVector2InRange(radius);// Random point ON sphere surface (uniform distribution)Vector3onSphere=rng.NextVector3OnSphere(radius);// Uses Marsaglia's method for true uniform distribution// Random rotation (uniform distribution)Quaternionrotation=rng.NextQuaternion();// Uses Shoemake's algorithm
// Random opaque colorColorcolor=rng.NextColor();// Random color in HSV range (for similar hues)Colortint=rng.NextColorInRange(baseColor:Color.red,hueVariance:0.1f,saturationVariance:0.2f,valueVariance:0.2f);
// Select 5 random enemies from potentially huge listIEnumerable<Enemy>allEnemies=GetAllEnemiesInWorld();List<Enemy>randomFive=rng.NextSubset(allEnemies,k:5);// O(n) time, uses reservoir sampling for uniform probability
usingWallstopStudios.UnityHelpers.Core.Extension;usingUnityEngine.SceneManagement;// ✅ Now you can await scene loadingasyncTaskLoadGameScene(){varoperation=SceneManager.LoadSceneAsync("GameLevel");awaitoperation;Debug.Log("Scene loaded!");}
Note: Unity 2023.1+ has built-in await support, but this works in older versions.
// As TaskTasktask=asyncOperation.AsTask();awaittask;// As ValueTask (reduces allocations for short operations)ValueTaskvalueTask=asyncOperation.AsValueTask();awaitvalueTask;
usingWallstopStudios.UnityHelpers.Core.Extension;asyncTask<string>DownloadDataAsync(){// Some async operation (HttpClient, database, etc.)awaitTask.Delay(1000);return"Downloaded data";}// In MonoBehaviourIEnumeratorStart(){// ✅ Convert Task to IEnumeratorreturnDownloadDataAsync().AsCoroutine();}