Numerics and vectors
Small value types and helpers for the arithmetic game and drawing code runs every frame: vectors and
rectangles, scalar math, overlap tests, seedable randomness, a spatial index, and the 3D camera, ray and
bounding-volume set. Everything here lives in SharpProspero.Numerics and works in single precision.
On this page
Vectors
Vector2 is a small value type for a position, a velocity or a direction, so movement and steering read as
arithmetic rather than a pair of loose floats.
var position = new Vector2(100f, 60f);
var velocity = Vector2.UnitX * 240f; // 240 px/sec to the right
position += velocity * (float)context.DeltaSeconds; // advance by the frame time
float away = Vector2.Distance(position, target);
Vector2 toward = (target - position).Normalized();
It has the usual operators, Length/LengthSquared, Normalized, Dot, Distance/DistanceSquared,
Lerp and Rotate, and the Zero, One, UnitX and UnitY constants. For steering there is
MoveTowards (advance toward a target by a capped step), ClampLength (cap the magnitude),
Perpendicular (a quarter turn), Cross (the perpendicular dot product, whose sign gives turn direction),
and the angle pair FromAngle/ToAngle. WithX/WithY return a copy with one component changed.
Rectangles
RectF is an axis-aligned rectangle for a bounding box or a hit area. It answers whether it holds a point
or another rectangle, where two rectangles overlap, and pulls a point to the nearest spot inside.
var button = new RectF(x, y, w, h);
if (button.Contains(cursor)) Press(); // point in rectangle
Vector2 snapped = playfield.Clamp(ball); // nearest spot inside
RectF overlap = a.Intersection(b); // Empty when they miss
RectF has Contains, Intersects, Intersection, Union, Inflate, Offset and Clamp, plus the
FromEdges and FromCenter constructors, the Empty value, and the read-only
Left/Top/Right/Bottom, Position, Center, Size and IsEmpty members. Edges follow the
half-open rule — top and left are inside, bottom and right
are not — so rectangles that share an edge do not both claim it.
Scalar math
MathUtil holds the floating-point helpers that go with the vectors: blending between values, mapping a
value from one range to another, easing an edge, and stepping toward a target.
float health = MathUtil.Clamp01(hp / maxHp);
float eased = MathUtil.SmoothStep(0f, 1f, t);
float volume = MathUtil.Remap(distance, 0f, 400f, 1f, 0f); // near = loud, far = silent
float facing = MathUtil.WrapAngle(heading + turn); // back into -pi..pi
It provides Lerp and LerpClamped, InverseLerp and Remap, SmoothStep, MoveTowards,
Clamp/Clamp01, Approximately, Repeat and PingPong, the angle helpers DegreesToRadians,
RadiansToDegrees and WrapAngle, and the Pi, TwoPi, DegreesPerRadian and RadiansPerDegree
constants.
For a value that should ease toward a target and settle without overshooting - a camera that follows the
player, a slider that glides to its new spot - SmoothDamp is the one to reach for. It carries a velocity
between calls, so keep that in a field and pass it by reference each frame; smoothTime is roughly how
long the move takes. SmoothDampAngle does the same for a heading, and LerpAngle blends two angles the
short way round. Vector2.SmoothDamp smooths a position with the same feel.
Vector2 cameraVelocity; // kept between frames
cameraTarget = player.Position;
cameraPos = Vector2.SmoothDamp(cameraPos, cameraTarget, ref cameraVelocity, smoothTime: 0.25f, deltaTime);
Collision tests
Collision is a static class of the overlap tests game code reaches for, over Vector2 and RectF:
circles, a circle against a rectangle, a point in a circle, and where two line segments cross.
Rectangle-against-rectangle and point-in-rectangle live on RectF itself.
if (Collision.CircleOverlapsRect(ball, radius, paddle)) Bounce();
if (Collision.CirclesOverlap(a, ra, b, rb)) Hit();
if (Collision.SegmentIntersection(eye, target, wallA, wallB, out Vector2 where))
DrawSpark(where); // line of sight blocked here
The full set is PointInCircle, CirclesOverlap, CircleOverlapsRect, SegmentsIntersect,
SegmentIntersection (with the crossing point as an out), and SegmentIntersectsRect for a segment that
touches or enters a rectangle. A segment test reports parallel or collinear lines as no single crossing.
Randomness
There are two random sources. GameRandom is a fast, reproducible generator for gameplay; the same seed
always yields the same sequence, so a replay or a procedural level comes out identical. HardwareEntropy
draws unpredictable bytes from the system for seeds.
var rng = new GameRandom(seed: 1234); // same seed, same sequence
int roll = rng.Next(1, 7); // 1..6 (max is exclusive)
double t = rng.NextDouble(); // 0..1
bool crit = rng.NextBool(0.1); // true 10% of the time
var unpredictable = GameRandom.FromEntropy(); // seeded from the system
ulong token = HardwareEntropy.NextUInt64(); // straight from the entropy source
GameRandom gives NextUInt64, NextUInt32, NextDouble (0 to 1), Next(max) and Next(min, max)
(max exclusive),
NextSingle (with an optional range), NextBool, Pick (one item from a set) and Shuffle (reorders a
span in place). Pick and Shuffle take spans, so they work over an array or a slice without allocating.
ReadOnlySpan<string> loot = ["sword", "shield", "potion"];
string drop = rng.Pick(loot); // one of the three, evenly
rng.Shuffle(deck); // deck is a Span<Card>, reordered in place
HardwareEntropy fills a span with Fill or hands back a value with NextUInt64; seed a GameRandom
from it when you want an unpredictable start.
For a draw that is not even - a loot table, a drop chart, a random-encounter list - WeightedTable<T> gives
each entry a weight and returns one with a chance proportional to it. It draws through a GameRandom, so a
seeded run repeats exactly. Weights are any non-negative numbers; an entry at weight 3 comes up three times
as often as one at weight 1.
var loot = new WeightedTable<string>()
.Add("common", 70)
.Add("uncommon", 25)
.Add("rare", 5);
string drop = loot.Pick(rng); // "common" about 70% of the time
loot.TryPick(rng, out string safe); // false instead of throwing on an empty table
Add chains, Count and TotalWeight report the contents, Clear empties it, and an entry at weight 0
is kept but never drawn.
Coherent noise
Where GameRandom gives independent values, NoiseField gives smooth ones: sampling nearby coordinates
returns nearby values, so it draws terrain heights, cloud and marble textures, and organic motion rather
than static. The same seed and coordinate always return the same value (-1 to 1), so a world is
reproducible.
var noise = new NoiseField(seed: 2024);
float height = noise.Noise2D(x * 0.05f, z * 0.05f); // one smooth layer
float terrain = noise.FractalNoise2D(x * 0.02f, z * 0.02f, octaves: 5); // layered detail
float density = noise.Noise3D(x, y, z); // caves, clouds
Noise2D/Noise3D are one layer; FractalNoise2D sums octaves at rising frequency and falling amplitude
for detail, with persistence and lacunarity to shape it. Scale the input coordinates to set the
feature size - smaller multipliers give broader shapes.
GameRandomis for gameplay, not for keys, tokens or anything that must resist guessing. Take those bytes fromHardwareEntropy. For hashing and checksums see Hashing and checksums.
Spatial queries
When a scene holds many things, testing every pair is wasteful. Quadtree<T> indexes items by rectangle
and answers “what is in this area” by visiting only the parts of the world near the query — collision
broad-phase, off-screen culling, or picking under the cursor.
var tree = new Quadtree<Entity>(worldBounds);
foreach (Entity e in entities)
tree.Insert(e, e.Bounds); // rebuild each frame for moving items
foreach (Entity near in tree.Query(visibleArea))
near.Draw(surface);
Query(RectF) returns a fresh List<T>; the Query(RectF, List<T>) overload appends into a list you
own, so a per-frame query reuses one buffer instead of allocating — the kind of discipline the
Memory page argues for. Insert grows the tree, Count
reports the item total, and Clear empties it while keeping the same bounds and limits. The constructor
takes optional maxItemsPerNode and maxDepth limits that bound how finely it subdivides. An item that
straddles a split line is kept at that level rather than duplicated, so a query never returns the same item
twice.
Quadtree<T>groups by rectangle, not by exact shape. Treat a hit as a candidate and follow it with the precise test —Collision.CirclesOverlaporRectF.Intersects— on the pairs it returns.
For curved motion, Spline evaluates Bezier and Catmull-Rom curves for a camera path or a projectile arc.
It lives in SharpProspero.Animation and is covered on the Animation page.
3D math
The same namespace carries the 3D set the renderer runs on. Camera3D holds a position, a target and the
projection settings and hands back View, Projection and ViewProjection matrices, with Perspective
and Orthographic builders, WorldToScreen for placing a marker over an object, and ScreenToRay for
picking under the cursor. Transform is a position, rotation and scale with a Matrix, the
Forward/Right/Up axes, LookAt, Rotate and TransformPoint/TransformDirection.
var camera = new Camera3D { Position = new Vector3(0, 1.5f, 4.5f), AspectRatio = 1920f / 1080f };
Ray pick = camera.ScreenToRay(cursorX, cursorY, 1920, 1080);
if (pick.IntersectSphere(target.Bounds) >= 0f) // negative means the ray misses
Select(target);
Ray intersects a plane, a sphere, a box or a triangle and returns the distance along itself to the hit,
negative for a miss; a ray that starts inside a sphere reports 0. BoundingBox builds from a point cloud
with FromPoints, grows with Encapsulate, re-fits around a matrix with Transform(Matrix4x4), and
answers Contains and Intersects. BoundingSphere shares FromPoints, Contains and Intersects, and
adds FromBox for the sphere around a box. Frustum builds from a view-projection matrix and tests a
point, a sphere or a box against the six planes, so only what the camera can see is drawn. See
GPU command layer for the renderer that consumes them.
Packing rectangles
RectPacker fits many small rectangles into one larger area without overlap - the job behind building a
sprite sheet or a glyph atlas out of separate images. It works in whole pixels and fills bottom-left along a
running skyline, which keeps the result tight. Give it a size and an id per piece, and it reports where each
one landed so you can copy the image in and record the region.
using var atlas = new PixelBuffer(1024, 1024);
Surface sheet = atlas.AsSurface();
var packer = new RectPacker(1024, 1024);
foreach ((int id, Surface image) in sprites)
{
PackedRect? slot = packer.Insert(image.Width, image.Height, id);
if (slot is { } r)
sheet.Blit(image, r.X, r.Y); // and remember (r.X, r.Y, r.Width, r.Height)
}
Insert returns null when a piece will not fit in the space left. Pack takes a whole batch, sorts it
largest-first for a tighter fit, and returns the pieces that fit - compare that count with the number you
gave to see whether any were too big. Occupancy is the fraction of the area filled, and Reset clears
it to pack again.