Goal: Build a tiny 2D engine using value types correctly and observe pitfalls/perf.
Tasks
- Implement a
readonly struct Vector2with fieldsdouble X, Y, constructor,Length,Normalized(), and operators+,-, unary-, scalar*,Dot(in Vector2 a, in Vector2 b), andDeconstruct(out double x, out double y). Ensure no hidden copies and no boxing. - Implement a
struct BoundingBoxwith fieldsVector2 Min, Max(mutable is fine here). AddContains(in Vector2 p)andInflate(double delta). - Implement a
class ParticlewithVector2 Position,Vector2 Velocity. Addvoid Step(double dt, in BoundingBox bounds)that moves the particle and bounces on edges (invert velocity on collision). - Write a
Mainthat creates 100 particles with random velocities inside a 100×100 box and advances them for 1,000 steps, accumulating the total distance traveled. - Pitfall demo: Add a
public Vector2 this[int i] { get; set; }indexer to aParticlescontainer that stores aList<Vector2>positions. Show thatforeach (var pos in positions)then mutatingpos.X++does not change the stored element; then fix it by using indexing orforwith the assignment.
Acceptance Criteria
Vector2isreadonly struct, methods that read only take parameters asin.- No allocation or boxing in the hot loop (avoid
object,ArrayList, LINQ in the inner step). - The simulation runs and prints the total distance traveled and the final average speed.
Hints — Vector Math & Simulation (Guided)
Hints - General
General tips
- Keep
Vector2a readonly struct and useinparameters where you read but don’t mutate to avoid copies. - Avoid LINQ, closures, and allocations inside the inner simulation loop. Pre‑allocate collections with capacity.
- Prefer inclusive bounds (
>= Minand<= Max) so particles resting on edges are considered inside.
Task 1 — Vector2
Task 1 — Vector2
- Length:
Math.Sqrt(X*X + Y*Y). - Normalized(): divide by length when length > 0, otherwise return
(0,0)to avoidNaN. - Operator hints (signatures):
public static Vector2 operator +(in Vector2 a, in Vector2 b)public static Vector2 operator -(in Vector2 a, in Vector2 b)public static Vector2 operator *(in Vector2 a, double k)
Dot(in a, in b)returnsa.X*b.X + a.Y*b.Y.Deconstruct(out double x, out double y)assignsx = X; y = Y;.
Task 2 — BoundingBox
Task 2 — BoundingBox
Contains(p):p.X >= Min.X && p.X <= Max.X && p.Y >= Min.Y && p.Y <= Max.Y.Inflate(d):Min = (Min.X - d, Min.Y - d)andMax = (Max.X + d, Max.Y + d).- Consider a constructor guard to ensure
Min <= Maxon both axes if inputs might be unsorted.
Task 3 — Particle.Step
Task 3 — Particle.Step
- Integrate:
next = Position + Velocity * dt. - Bounce X:
- If
next.X < bounds.Min.X: setnext.X = bounds.Min.Xand makevx = +Math.Abs(vx). - If
next.X > bounds.Max.X: setnext.X = bounds.Max.Xand makevx = -Math.Abs(vx).
- If
- Bounce Y similarly with
vy. - Finally assign
Position = next; Velocity = (vx, vy);. - If particles occasionally “tunnel” through edges, reduce
dtor iterate collision resolution per axis.
Task 4 — Simulation loop
Task 4 — Simulation loop
- Create particles with random positions in
[0,100]and velocities asdir.Normalized() * speed. - Accumulate distance as
total += p.Speed * dteach step. - Use
new List<Particle>(capacity: 100)to avoid resizes; seedRandomfor reproducibility.
Mutable‑struct pitfall demo
Task 5 — Mutable‑struct pitfall demo
- In
foreach (var pos in positions),posis a copy (value type); changing it doesn’t modify the stored element. - Show the issue, then fix by:
- Using an indexer:
var tmp = list[i]; /* mutate tmp */ list[i] = tmp;, or - Switching to arrays/
Span<T>if you needrefaccess;List<T>does not exposerefelements.
- Using an indexer:
Testing & verification ideas
Testing & verification ideas
- Assert
Vector2.Dot(a,b) == Vector2.Dot(b,a)and thatNormalized().Lengthis ~1 (within epsilon). - After each step, assert
bounds.Contains(p.Position); if false, check bounce/clamp math. - Log a few positions/velocities at edges to verify sign inversions.
Performance checks (optional)
- Ensure the hot loop allocates 0 bytes (no boxing/closures). You can spot accidental allocations by avoiding
stringconcatenation inside loops and keepingToString()calls out of the hot path.