A C# library of utility types, math helpers, and game-systems primitives for .NET 10. Primarily aimed at game development, with a focus on performance and zero unnecessary allocations.
dotnet add package JST.GameUtils
- Contributing
- Math
- Vector extensions
- Geometry
- Collections
- Entity / Game systems
- Animation
- Procedural generation
- Types
- Extensions
- Terminal
- Performance is a key goal. Avoid unnecessary allocations, prefer structs for small types, and consider cache locality.
- Add XML documentation comments to all public members for better IntelliSense support.
- Add tests for all public APIs, covering edge cases and typical usage patterns.
- Update the README with usage examples for any new features or types added.
MathFExt.Lerp(0f, 10f, 0.5f); // 5f
MathFExt.InverseLerp(0f, 10f, 5f); // 0.5f
MathFExt.Smoothstep(0f, 1f, 0.5f);
MathFExt.Smootherstep(0f, 1f, 0.5f);
MathFExt.Remap(value, 0f, 1f, -1f, 1f);
MathFExt.RemapClamped(value, 0f, 1f, -1f, 1f);
MathFExt.Wrap(value, min, max);
MathFExt.Normalize(value, min, max);
MathFExt.PingPong(t, length);
MathFExt.AngleDifference(fromAngle, toAngle);
MathFExt.ToRadians(degrees);
MathFExt.ToDegrees(radians);Constants: DEGREES_PER_RADIANS, RADIANS_PER_DEGREE, HALF_PI.
MathExt.RandomFloat(); // [0, 1)
MathExt.RandomFloat(min, max);
MathExt.RandomInt(min, max);
MathExt.RandomBool();
MathExt.RandomGaussian(mean, stddev);
MathExt.RandomInCircle(radius); // random point inside circle
MathExt.RandomOnCircle(radius); // random point on circle edgeAll functions take float t in [0, 1] and return a float.
Available families: Sine, Quad, Cubic, Quart, Quint, Expo, Circ, Back, Elastic, Bounce.
Each has In, Out, and InOut variants.
float t = Easing.CubicInOut(progress);
float t = Easing.BounceOut(progress);
float t = Easing.ElasticIn(progress);Vector2 point = Bezier.Quadratic(p0, p1, p2, t);
Vector2 point = Bezier.Cubic(p0, p1, p2, p3, t);
Vector2 tangent = Bezier.CubicTangent(p0, p1, p2, p3, t);
var path = new BezierPath();
path.AddSegment(p0, p1, p2, p3);
Vector2 point = path.GetPoint(t); // t over entire path [0, 1]
Vector2 tangent = path.GetTangent(t);Unlike Bézier curves, Catmull-Rom splines pass through every control point — ideal for camera paths, patrol routes, and cutscenes.
// Single segment
Vector2 point = CatmullRom.Sample(p0, p1, p2, p3, t);
Vector2 tangent = CatmullRom.Tangent(p0, p1, p2, p3, t);
// Multi-point path
var path = new CatmullRomPath()
.AddPoint(new Vector2(0, 0))
.AddPoint(new Vector2(5, 3))
.AddPoint(new Vector2(10, 0))
.SetLooping(false);
Vector2 pos = path.GetPoint(0.5f); // midpoint of path
Vector2 dir = path.GetTangent(0.5f);var bag = new ShuffleBag<string>();
bag.Add("common", weight: 10);
bag.Add("rare", weight: 1);
string item = bag.Next();
bag.Reset();vec.Rotate(radians);
vec.Rotate(radians, origin);
vec.Add(scalar);
vec.Add(x, y);
vec.Floor(); vec.Ceil(); vec.Round();
vec.Perpendicular();
vec.WithX(x); vec.WithY(y);
vec.IsZero();
vec.ToAngle();
vec.AngleTowards(target);
vec.AngleBetween(other);
vec.GetDirection(target);
vec.MoveTowards(target, maxDelta);
vec.ToVector3(z);
vec.Deconstruct(out x, out y);
// On IEnumerable<Vector2>:
vectors.Midpoint();
vectors.Sort(clockwise: true);
vectors.Sort(center, clockwise: true);Includes WithX, WithY, WithZ, ToVector2, Deconstruct, and other component helpers.
All geometry types are in GameUtils.Types.Geometry.
var box = new AABB(min, max);
box.Contains(point);
box.Intersects(other); // AABB, Line, Circle, Polygon2D, Vector2[]
box.Intersects(line, out intersectionPoints);var circle = new Circle(center, radius);
circle.Contains(point);
circle.Intersects(aabb); // AABB, Line, Circle, Polygon2D
circle.RadiusSquared; // cached, avoids sqrt in hot pathsvar line = new Line(start, end);
var line = Line.FromAngle(origin, angle, length);
line.Length; line.Midpoint; line.NormalA; line.NormalB;
line.Intersects(other); // Line, AABB, Circle, Polygon2D
line.Cast(maxDistance, shapes); // first intersection along rayvar ray = new Ray2D(origin, direction);
ray.At(distance);
ray.Intersects(line, out t, out point);
ray.Intersects(circle, out t, out point);
ray.Intersects(aabb, out t, out point);var poly = new Polygon2D(vertices); // auto-sorts CCW
poly.Vertices; poly.Edges; poly.Normals;
poly.Contains(point);
poly.Intersects(aabb);
poly.TranslateBy(delta);var quad = new Quad(topLeft, topRight, bottomLeft, bottomRight);
quad.Intersects(line, out point);
quad[index]; // corner by index 0–3var grid = new Grid<int>(width, height);
grid[x, y] = 1;
grid[vector2] = 1;
grid.TryGet(x, y, out value);
grid.Fill(defaultValue);
grid.Fill((x, y) => ComputeValue(x, y));
grid.IsInBounds(x, y);
foreach (var (x, y, value) in grid) { ... }Best for non-uniform object distributions (e.g. world objects, static geometry).
var tree = new QuadTree<Enemy>(worldBounds, capacity: 8);
tree.Insert(enemy, enemy.Position);
tree.Query(camera.GetVisibleBounds()); // IEnumerable<Enemy>
tree.Query(explosionCenter, blastRadius);
tree.Remove(enemy, enemy.Position);
tree.Clear();Best for large numbers of uniformly distributed dynamic objects (bullets, particles, units). O(1) amortized insert and query.
var hash = new SpatialHash<Bullet>(cellSize: 64f);
hash.Insert(bullet, bullet.Position);
hash.Query(region); // IEnumerable<Bullet>
hash.Query(center, radius);
hash.Remove(bullet, bullet.Position);
hash.Clear();var buffer = new RingBuffer<float>(capacity: 64);
buffer.Write(sample);
float value = buffer.Read();
buffer.TryRead(out value);
buffer.Peek();
buffer.Snapshot(); // copy of all current elementsThread-safe set types for use in multi-threaded game systems.
var fsm = new StateMachine<State>(State.Idle);
fsm.AddTransition(State.Idle, State.Run, () => speed > 0);
fsm.OnEnter(State.Run, () => PlayAnim("run"));
fsm.OnExit(State.Run, () => StopAnim());
fsm.Update(); // evaluates transitions and fires callbacks
fsm.ForceState(State.Dead);var pool = new ObjectPool<Bullet>(() => new Bullet());
var bullet = pool.Rent();
// ... use bullet ...
pool.Return(bullet);Zero-allocation on the dispatch path (copy-on-write snapshot rebuilt only on subscribe/unsubscribe).
var bus = new EventBus();
bus.Subscribe<PlayerDiedEvent>(e => HandleDeath(e));
bus.Publish(new PlayerDiedEvent { Position = pos });
bus.Unsubscribe<PlayerDiedEvent>(handler);
bus.Clear<PlayerDiedEvent>();// Float tween with cubic ease-out
var tween = Tween.Float(0f, 100f, duration: 1.5f, Easing.CubicOut);
float value = tween.Update(deltaTime);
if (tween.IsComplete) { ... }
tween.Reset();
tween.Reverse();
// Vector2 tween
var move = Tween.Vec2(start, end, 2f, Easing.SineInOut);
// Color tween
var fade = Tween.Color(Color.White, Color.Transparent, 0.5f);var tree = new BehaviorTree(
new Selector(
new Sequence(new IsEnemyVisible(), new ChaseEnemy()),
new Patrol()
)
);
tree.Tick(context);Node types: Sequence, Selector, Inverter, Leaf (for custom logic).
var astar = new AStar<Vector2Int>();
astar.AddEdge(new Edge<Vector2Int>(a, b, cost));
astar.Solve(start, goal, (a, b) => Vector2.Distance(a, b), out var path);
var dijkstra = new Dijkstra<string>();
dijkstra.AddEdge(new Edge<string>("A", "B", 1.5f));
dijkstra.Solve("A", "D", out var path);GridSearch.BreadthFirstSearch(grid, start, isWalkable, out path);
GridSearch.FloodFill(grid, origin, condition, fillValue);var thinker = new Thinker(interval: 0.5f);
thinker.OnThink = () => UpdateAI();
thinker.Update(deltaTime);class PhysicsLoop : FixedScheduler
{
public PhysicsLoop() : base(tickRate: 60) { }
protected override void Update() => StepPhysics(1f / 60f);
}
var loop = new PhysicsLoop();
loop.Start();
// ... later ...
loop.Stop();var anim = new Controller(frameCount: 10, isLooping: true, framesPerSecond: 12);
anim.OnFrameChanged = frame => currentSprite = sprites[frame];
anim.Play();
anim.Update(deltaTime);var tween = new Tweener(from: 0f, to: 100f, duration: 1.5f, easingFunction: Ease.CubicOut);
tween.OnComplete = () => Console.WriteLine("Done!");
tween.Update(deltaTime);Ease contains easing functions like Ease.ElasticOut, Ease.BounceIn, etc.
Offset contains math functions for offsetting animations (Jagged, Sine, Pulse, Triangle, Wobble).
var noise = new PerlinNoise(); // standard Ken Perlin table
var noise = new PerlinNoise(seed: 42); // seeded / reproducible
float v = noise.Sample(x, y); // 2D, [0, 1]
float v = noise.Sample(x, y, z); // 3D, [-1, 1]
float v = noise.Fbm(x, y, octaves: 6); // fractal Brownian motion, [0, 1]
// Or use the shared default instance:
PerlinNoise.Default.Sample(x, y);Grid<int> heightmap = Diamond.Create(
size: 257, // must be power-of-two + 1
min: 0, max: 255,
range: 128f,
nextRange: r => r * 0.5f,
valueFactory: (avg, range) => (int)(avg + Random.Shared.NextSingle() * range),
seed: 42
);var c = new Color(r: 1f, g: 0f, b: 0.5f, a: 1f);
var c = new Color(byte r, byte g, byte b, byte a);
Color.Lerp(a, b, t);
Color.Red; Color.SkyBlue; // 140+ named CSS colors
(Vector3)c; (Vector4)c; // explicit casts
Color.FromRgba(0xFF8800FF);var gradient = new Gradient();
gradient.AddStop(0f, Color.Black)
.AddStop(0.5f, Color.Red)
.AddStop(1f, Color.White);
Color c = gradient.Evaluate(0.75f);var cam = new Camera2D { Position = pos, Zoom = 1.5f, Rotation = 0f };
Vector2 screen = cam.WorldToScreen(worldPos);
Vector2 world = cam.ScreenToWorld(screenPos);
AABB visible = cam.GetVisibleBounds();Low-level pixel buffers. Bitmap supports drawing primitives (Rectangle, Line, Circle) and writing BMP files. ImageData is a compact RGBA container.
list.GetRandom();
list.Shuffle();
list.WeightedRandom(item => item.Weight);
list.ForEach(item => DoSomething(item));
list.SelectWhere(item => item.IsActive, item => item.Value);
list.ToIndex(); // Dictionary<T, int>obj.Mutate(o => o.X = 5); // mutate and return same object
obj.Out(out var local); // assign to local, keep chain
obj.Curry(arg1); // partial applicationstr.TryGet(index, out char c);
str.Repeat(3);Ansi.WriteLine("Hello!", Ansi.Bold + Ansi.Foreground(Color.Cyan));
Ansi.MoveTo(x: 10, y: 5);
Ansi.Clear();
Ansi.ClearLine();Progress.Bar(current, total, width: 40);
Progress.PercentComplete(current, total);
Progress.Rate(itemsProcessed, elapsed);
Progress.TimeRemaining(current, total, elapsed);