3 - Physics Simulation
Physics simulation is a crucial aspect of game development, providing realistic movement, collisions, and interactions between game objects. This section covers the fundamental algorithms and techniques used for physics simulation in games.
Introduction to Game Physics
Game physics simulations typically focus on:
- Rigid Body Dynamics: Simulating solid objects that don't deform.
- Collision Detection: Determining when objects intersect.
- Collision Response: Calculating how objects react after colliding.
- Constraints: Enforcing relationships between objects (joints, limits, etc.).
- Particle Systems: Simulating large numbers of small particles.
While many game engines provide built-in physics systems, understanding the underlying algorithms helps in customizing behavior, optimizing performance, and debugging issues.
Basic Physics Concepts
Vectors and Forces
public struct Vector2
{
public float x;
public float y;
public Vector2(float x, float y)
{
this.x = x;
this.y = y;
}
public static Vector2 zero => new Vector2(0, 0);
public float magnitude => MathF.Sqrt(x * x + y * y);
public Vector2 normalized => magnitude > 0 ? this / magnitude : zero;
public static Vector2 operator +(Vector2 a, Vector2 b) => new Vector2(a.x + b.x, a.y + b.y);
public static Vector2 operator -(Vector2 a, Vector2 b) => new Vector2(a.x - b.x, a.y - b.y);
public static Vector2 operator *(Vector2 a, float b) => new Vector2(a.x * b, a.y * b);
public static Vector2 operator /(Vector2 a, float b) => new Vector2(a.x / b, a.y / b);
public static float Dot(Vector2 a, Vector2 b) => a.x * b.x + a.y * b.y;
public static float Cross(Vector2 a, Vector2 b) => a.x * b.y - a.y * b.x;
public static float Distance(Vector2 a, Vector2 b) => (b - a).magnitude;
}
Newton's Laws of Motion
- First Law: An object at rest stays at rest, and an object in motion stays in motion unless acted upon by a force.
- Second Law: Force equals mass times acceleration (F = ma).
- Third Law: For every action, there is an equal and opposite reaction.
Rigid Body Dynamics
Rigid body dynamics simulate the movement of solid objects under the influence of forces.
Basic Rigid Body Implementation
public class RigidBody
{
// Physical properties
public float Mass { get; set; } = 1.0f;
public float InverseMass => Mass > 0 ? 1.0f / Mass : 0.0f;
public float Restitution { get; set; } = 0.5f; // Bounciness
public float StaticFriction { get; set; } = 0.5f;
public float DynamicFriction { get; set; } = 0.3f;
// State variables
public Vector2 Position { get; set; }
public Vector2 Velocity { get; set; }
public float Rotation { get; set; } // In radians
public float AngularVelocity { get; set; }
// Forces
public Vector2 Force { get; private set; }
public float Torque { get; private set; }
// Moment of inertia (for rotation)
public float MomentOfInertia { get; set; }
public float InverseMomentOfInertia => MomentOfInertia > 0 ? 1.0f / MomentOfInertia : 0.0f;
// Is this body fixed in place?
public bool IsStatic { get; set; }
public RigidBody(float mass, Vector2 position)
{
Mass = mass;
Position = position;
Velocity = Vector2.zero;
Rotation = 0;
AngularVelocity = 0;
Force = Vector2.zero;
Torque = 0;
// For a circle
MomentOfInertia = 0.5f * mass * 1.0f; // Assuming radius = 1 for simplicity
IsStatic = mass <= 0;
}
public void ApplyForce(Vector2 force)
{
Force += force;
}
public void ApplyForceAtPoint(Vector2 force, Vector2 point)
{
Force += force;
// Calculate torque
Vector2 arm = point - Position;
Torque += Vector2.Cross(arm, force);
}
public void ApplyImpulse(Vector2 impulse)
{
if (IsStatic)
return;
Velocity += impulse * InverseMass;
}
public void ApplyImpulseAtPoint(Vector2 impulse, Vector2 point)
{
if (IsStatic)
return;
Velocity += impulse * InverseMass;
Vector2 arm = point - Position;
AngularVelocity += Vector2.Cross(arm, impulse) * InverseMomentOfInertia;
}
public void Update(float deltaTime)
{
if (IsStatic)
return;
// Update linear motion
Vector2 acceleration = Force * InverseMass;
Velocity += acceleration * deltaTime;
Position += Velocity * deltaTime;
// Update angular motion
float angularAcceleration = Torque * InverseMomentOfInertia;
AngularVelocity += angularAcceleration * deltaTime;
Rotation += AngularVelocity * deltaTime;
// Reset forces and torque
Force = Vector2.zero;
Torque = 0;
}
}
Integration Methods
Different integration methods can be used to update the position and velocity of objects:
Euler Integration
The simplest method, but less accurate for fast-moving objects.
public void UpdateEuler(float deltaTime)
{
if (IsStatic)
return;
// Update linear motion
Vector2 acceleration = Force * InverseMass;
Velocity += acceleration * deltaTime;
Position += Velocity * deltaTime;
// Update angular motion
float angularAcceleration = Torque * InverseMomentOfInertia;
AngularVelocity += angularAcceleration * deltaTime;
Rotation += AngularVelocity * deltaTime;
// Reset forces and torque
Force = Vector2.zero;
Torque = 0;
}
Verlet Integration
More stable for physics simulations.
public class VerletRigidBody
{
public float Mass { get; set; } = 1.0f;
public float InverseMass => Mass > 0 ? 1.0f / Mass : 0.0f;
public Vector2 Position { get; set; }
public Vector2 PreviousPosition { get; set; }
public Vector2 Acceleration { get; set; }
public bool IsStatic { get; set; }
public VerletRigidBody(float mass, Vector2 position)
{
Mass = mass;
Position = position;
PreviousPosition = position;
Acceleration = Vector2.zero;
IsStatic = mass <= 0;
}
public void ApplyForce(Vector2 force)
{
Acceleration += force * InverseMass;
}
public void Update(float deltaTime)
{
if (IsStatic)
return;
// Store current position
Vector2 temp = Position;
// Update position using Verlet integration
float deltaTimeSquared = deltaTime * deltaTime;
Position = 2 * Position - PreviousPosition + Acceleration * deltaTimeSquared;
// Update previous position
PreviousPosition = temp;
// Reset acceleration
Acceleration = Vector2.zero;
}
// Calculate velocity (if needed)
public Vector2 GetVelocity(float deltaTime)
{
return (Position - PreviousPosition) / deltaTime;
}
}
RK4 Integration
Runge-Kutta 4th order method, more accurate but computationally expensive.
public class RK4RigidBody
{
public float Mass { get; set; } = 1.0f;
public float InverseMass => Mass > 0 ? 1.0f / Mass : 0.0f;
public Vector2 Position { get; set; }
public Vector2 Velocity { get; set; }
public Vector2 Force { get; private set; }
public bool IsStatic { get; set; }
public RK4RigidBody(float mass, Vector2 position)
{
Mass = mass;
Position = position;
Velocity = Vector2.zero;
Force = Vector2.zero;
IsStatic = mass <= 0;
}
public void ApplyForce(Vector2 force)
{
Force += force;
}
public void Update(float deltaTime)
{
if (IsStatic)
return;
// RK4 integration
Vector2 k1v = Force * InverseMass * deltaTime;
Vector2 k1p = Velocity * deltaTime;
Vector2 k2v = Force * InverseMass * deltaTime;
Vector2 k2p = (Velocity + k1v * 0.5f) * deltaTime;
Vector2 k3v = Force * InverseMass * deltaTime;
Vector2 k3p = (Velocity + k2v * 0.5f) * deltaTime;
Vector2 k4v = Force * InverseMass * deltaTime;
Vector2 k4p = (Velocity + k3v) * deltaTime;
Velocity += (k1v + k2v * 2 + k3v * 2 + k4v) / 6;
Position += (k1p + k2p * 2 + k3p * 2 + k4p) / 6;
// Reset forces
Force = Vector2.zero;
}
}
Collision Detection
Collision detection determines when objects intersect.
Broad Phase
Broad phase collision detection quickly identifies potential collisions to reduce the number of detailed checks.
Grid-Based Spatial Partitioning
public class SpatialGrid
{
private List<RigidBody>[,] grid;
private int cellSize;
private int width, height;
public SpatialGrid(int width, int height, int cellSize)
{
this.width = width;
this.height = height;
this.cellSize = cellSize;
int cols = width / cellSize + 1;
int rows = height / cellSize + 1;
grid = new List<RigidBody>[cols, rows];
for (int x = 0; x < cols; x++)
{
for (int y = 0; y < rows; y++)
{
grid[x, y] = new List<RigidBody>();
}
}
}
public void Insert(RigidBody body)
{
int cellX = (int)(body.Position.x / cellSize);
int cellY = (int)(body.Position.y / cellSize);
if (cellX >= 0 && cellX < grid.GetLength(0) && cellY >= 0 && cellY < grid.GetLength(1))
{
grid[cellX, cellY].Add(body);
}
}
public List<RigidBody> GetNearbyBodies(RigidBody body, float radius)
{
List<RigidBody> result = new List<RigidBody>();
int minCellX = (int)((body.Position.x - radius) / cellSize);
int maxCellX = (int)((body.Position.x + radius) / cellSize);
int minCellY = (int)((body.Position.y - radius) / cellSize);
int maxCellY = (int)((body.Position.y + radius) / cellSize);
minCellX = Math.Max(0, minCellX);
maxCellX = Math.Min(grid.GetLength(0) - 1, maxCellX);
minCellY = Math.Max(0, minCellY);
maxCellY = Math.Min(grid.GetLength(1) - 1, maxCellY);
for (int x = minCellX; x <= maxCellX; x++)
{
for (int y = minCellY; y <= maxCellY; y++)
{
foreach (var otherBody in grid[x, y])
{
if (body != otherBody)
{
result.Add(otherBody);
}
}
}
}
return result;
}
public void Clear()
{
for (int x = 0; x < grid.GetLength(0); x++)
{
for (int y = 0; y < grid.GetLength(1); y++)
{
grid[x, y].Clear();
}
}
}
}
Sweep and Prune
public class SweepAndPrune
{
private List<RigidBody> bodies = new List<RigidBody>();
private List<(RigidBody, RigidBody)> potentialCollisions = new List<(RigidBody, RigidBody)>();
public void AddBody(RigidBody body)
{
bodies.Add(body);
}
public void RemoveBody(RigidBody body)
{
bodies.Remove(body);
}
public List<(RigidBody, RigidBody)> DetectPotentialCollisions()
{
potentialCollisions.Clear();
if (bodies.Count < 2)
return potentialCollisions;
// Sort bodies by their x-coordinate
var sortedBodies = bodies.OrderBy(b => b.Position.x).ToList();
// Check for overlaps along the x-axis
for (int i = 0; i < sortedBodies.Count - 1; i++)
{
RigidBody bodyA = sortedBodies[i];
float maxX = bodyA.Position.x + 1.0f; // Assuming radius = 1 for simplicity
for (int j = i + 1; j < sortedBodies.Count; j++)
{
RigidBody bodyB = sortedBodies[j];
// If bodyB's minimum x is greater than bodyA's maximum x, no more overlaps are possible
if (bodyB.Position.x - 1.0f > maxX)
break;
// Check for overlap along the y-axis
float minY = Math.Max(bodyA.Position.y - 1.0f, bodyB.Position.y - 1.0f);
float maxY = Math.Min(bodyA.Position.y + 1.0f, bodyB.Position.y + 1.0f);
if (minY <= maxY)
{
potentialCollisions.Add((bodyA, bodyB));
}
}
}
return potentialCollisions;
}
}
Narrow Phase
Narrow phase collision detection performs detailed intersection tests between objects.
Circle-Circle Collision
public class CollisionDetector
{
public bool CheckCircleCircleCollision(RigidBody a, RigidBody b, out Vector2 normal, out float depth)
{
normal = Vector2.zero;
depth = 0;
// Calculate distance between centers
Vector2 direction = b.Position - a.Position;
float distance = direction.magnitude;
// Sum of radii (assuming radius = 1 for simplicity)
float sumRadii = 2.0f;
// Check if circles are colliding
if (distance >= sumRadii)
return false;
// Calculate collision normal and penetration depth
normal = distance > 0 ? direction / distance : new Vector2(1, 0);
depth = sumRadii - distance;
return true;
}
}
AABB-AABB Collision
public class AABB
{
public Vector2 Min { get; set; }
public Vector2 Max { get; set; }
public AABB(Vector2 min, Vector2 max)
{
Min = min;
Max = max;
}
public AABB(Vector2 center, float width, float height)
{
Min = new Vector2(center.x - width / 2, center.y - height / 2);
Max = new Vector2(center.x + width / 2, center.y + height / 2);
}
public bool Intersects(AABB other)
{
return Min.x <= other.Max.x && Max.x >= other.Min.x &&
Min.y <= other.Max.y && Max.y >= other.Min.y;
}
public bool Intersects(AABB other, out Vector2 normal, out float depth)
{
normal = Vector2.zero;
depth = 0;
if (!Intersects(other))
return false;
// Calculate overlap in x and y directions
float overlapX = Math.Min(Max.x, other.Max.x) - Math.Max(Min.x, other.Min.x);
float overlapY = Math.Min(Max.y, other.Max.y) - Math.Max(Min.y, other.Min.y);
// Use the minimum overlap as the penetration depth
if (overlapX < overlapY)
{
depth = overlapX;
normal = new Vector2(Min.x < other.Min.x ? -1 : 1, 0);
}
else
{
depth = overlapY;
normal = new Vector2(0, Min.y < other.Min.y ? -1 : 1);
}
return true;
}
}
SAT (Separating Axis Theorem)
public class Polygon
{
public Vector2[] Vertices { get; private set; }
public Vector2 Position { get; set; }
public float Rotation { get; set; }
public Polygon(Vector2[] vertices, Vector2 position, float rotation)
{
Vertices = vertices;
Position = position;
Rotation = rotation;
}
public Vector2[] GetTransformedVertices()
{
Vector2[] transformed = new Vector2[Vertices.Length];
float cos = MathF.Cos(Rotation);
float sin = MathF.Sin(Rotation);
for (int i = 0; i < Vertices.Length; i++)
{
// Rotate
float x = Vertices[i].x * cos - Vertices[i].y * sin;
float y = Vertices[i].x * sin + Vertices[i].y * cos;
// Translate
transformed[i] = new Vector2(x + Position.x, y + Position.y);
}
return transformed;
}
public Vector2[] GetAxes()
{
Vector2[] transformed = GetTransformedVertices();
Vector2[] axes = new Vector2[transformed.Length];
for (int i = 0; i < transformed.Length; i++)
{
Vector2 p1 = transformed[i];
Vector2 p2 = transformed[(i + 1) % transformed.Length];
// Get the edge vector
Vector2 edge = p2 - p1;
// Get the perpendicular vector (normal)
axes[i] = new Vector2(-edge.y, edge.x).normalized;
}
return axes;
}
public (float min, float max) Project(Vector2 axis)
{
Vector2[] transformed = GetTransformedVertices();
float min = float.MaxValue;
float max = float.MinValue;
foreach (var vertex in transformed)
{
float projection = Vector2.Dot(vertex, axis);
if (projection < min) min = projection;
if (projection > max) max = projection;
}
return (min, max);
}
}
public class SATCollisionDetector
{
public bool CheckPolygonPolygonCollision(Polygon a, Polygon b, out Vector2 normal, out float depth)
{
normal = Vector2.zero;
depth = float.MaxValue;
// Get axes from both polygons
Vector2[] axesA = a.GetAxes();
Vector2[] axesB = b.GetAxes();
// Check all axes from polygon A
foreach (var axis in axesA)
{
var projectionA = a.Project(axis);
var projectionB = b.Project(axis);
// Check for separation
if (projectionA.max < projectionB.min || projectionB.max < projectionA.min)
return false;
// Calculate penetration depth
float overlap = Math.Min(projectionA.max, projectionB.max) - Math.Max(projectionA.min, projectionB.min);
if (overlap < depth)
{
depth = overlap;
normal = axis;
// Ensure the normal points from A to B
Vector2 centerA = a.Position;
Vector2 centerB = b.Position;
if (Vector2.Dot(centerB - centerA, normal) < 0)
normal = normal * -1;
}
}
// Check all axes from polygon B
foreach (var axis in axesB)
{
var projectionA = a.Project(axis);
var projectionB = b.Project(axis);
// Check for separation
if (projectionA.max < projectionB.min || projectionB.max < projectionA.min)
return false;
// Calculate penetration depth
float overlap = Math.Min(projectionA.max, projectionB.max) - Math.Max(projectionA.min, projectionB.min);
if (overlap < depth)
{
depth = overlap;
normal = axis;
// Ensure the normal points from A to B
Vector2 centerA = a.Position;
Vector2 centerB = b.Position;
if (Vector2.Dot(centerB - centerA, normal) < 0)
normal = normal * -1;
}
}
return true;
}
}
Collision Response
Collision response calculates how objects react after colliding.
Impulse-Based Collision Response
public class CollisionResolver
{
public void ResolveCollision(RigidBody a, RigidBody b, Vector2 normal, float depth)
{
// Skip if both bodies are static
if (a.IsStatic && b.IsStatic)
return;
// Calculate relative velocity
Vector2 relativeVelocity = b.Velocity - a.Velocity;
// Calculate relative velocity along the normal
float velocityAlongNormal = Vector2.Dot(relativeVelocity, normal);
// Do not resolve if objects are separating
if (velocityAlongNormal > 0)
return;
// Calculate restitution (bounciness)
float restitution = Math.Min(a.Restitution, b.Restitution);
// Calculate impulse scalar
float j = -(1 + restitution) * velocityAlongNormal;
j /= a.InverseMass + b.InverseMass;
// Apply impulse
Vector2 impulse = normal * j;
a.ApplyImpulse(-impulse);
b.ApplyImpulse(impulse);
// Friction
// Calculate relative velocity again (after normal impulse)
relativeVelocity = b.Velocity - a.Velocity;
// Calculate tangent vector (perpendicular to normal)
Vector2 tangent = relativeVelocity - normal * Vector2.Dot(relativeVelocity, normal);
if (tangent.magnitude > 0.0001f)
{
tangent = tangent.normalized;
// Calculate friction impulse scalar
float jt = -Vector2.Dot(relativeVelocity, tangent);
jt /= a.InverseMass + b.InverseMass;
// Coulomb's law: friction <= mu * normal force
float mu = (a.StaticFriction + b.StaticFriction) * 0.5f;
Vector2 frictionImpulse;
if (Math.Abs(jt) < j * mu)
{
frictionImpulse = tangent * jt;
}
else
{
float dynamicFriction = (a.DynamicFriction + b.DynamicFriction) * 0.5f;
frictionImpulse = tangent * -j * dynamicFriction;
}
// Apply friction impulse
a.ApplyImpulse(-frictionImpulse);
b.ApplyImpulse(frictionImpulse);
}
// Positional correction to prevent sinking
const float percent = 0.2f; // Penetration percentage to correct
const float slop = 0.01f; // Penetration allowance
Vector2 correction = normal * Math.Max(depth - slop, 0) / (a.InverseMass + b.InverseMass) * percent;
a.Position -= correction * a.InverseMass;
b.Position += correction * b.InverseMass;
}
}
Position-Based Dynamics
public class PositionBasedDynamics
{
private List<VerletRigidBody> bodies = new List<VerletRigidBody>();
private List<(VerletRigidBody, VerletRigidBody)> collisions = new List<(VerletRigidBody, VerletRigidBody)>();
public void AddBody(VerletRigidBody body)
{
bodies.Add(body);
}
public void Update(float deltaTime)
{
// Apply forces and update positions
foreach (var body in bodies)
{
body.Update(deltaTime);
}
// Detect collisions
DetectCollisions();
// Resolve collisions
ResolveCollisions();
}
private void DetectCollisions()
{
collisions.Clear();
for (int i = 0; i < bodies.Count; i++)
{
for (int j = i + 1; j < bodies.Count; j++)
{
VerletRigidBody a = bodies[i];
VerletRigidBody b = bodies[j];
// Skip if both bodies are static
if (a.IsStatic && b.IsStatic)
continue;
// Simple circle collision detection
Vector2 direction = b.Position - a.Position;
float distance = direction.magnitude;
// Sum of radii (assuming radius = 1 for simplicity)
float sumRadii = 2.0f;
if (distance < sumRadii)
{
collisions.Add((a, b));
}
}
}
}
private void ResolveCollisions()
{
const int iterations = 5; // More iterations = more stable but slower
for (int iteration = 0; iteration < iterations; iteration++)
{
foreach (var (a, b) in collisions)
{
// Skip if both bodies are static
if (a.IsStatic && b.IsStatic)
continue;
Vector2 direction = b.Position - a.Position;
float distance = direction.magnitude;
// Sum of radii (assuming radius = 1 for simplicity)
float sumRadii = 2.0f;
if (distance < sumRadii)
{
Vector2 normal = distance > 0 ? direction / distance : new Vector2(1, 0);
float depth = sumRadii - distance;
// Calculate position correction
float totalInverseMass = a.InverseMass + b.InverseMass;
if (totalInverseMass <= 0)
continue;
Vector2 correction = normal * depth / totalInverseMass;
// Apply correction
a.Position -= correction * a.InverseMass;
b.Position += correction * b.InverseMass;
}
}
}
}
}
Constraints
Constraints enforce relationships between objects, such as joints or limits.
Distance Constraint
public class DistanceConstraint
{
public VerletRigidBody BodyA { get; }
public VerletRigidBody BodyB { get; }
public float RestLength { get; }
public float Stiffness { get; set; } = 1.0f;
public DistanceConstraint(VerletRigidBody bodyA, VerletRigidBody bodyB, float restLength)
{
BodyA = bodyA;
BodyB = bodyB;
RestLength = restLength;
}
public void Solve()
{
Vector2 direction = BodyB.Position - BodyA.Position;
float distance = direction.magnitude;
if (distance <= 0.0001f)
return;
Vector2 normal = direction / distance;
float totalInverseMass = BodyA.InverseMass + BodyB.InverseMass;
if (totalInverseMass <= 0)
return;
float difference = (distance - RestLength) / totalInverseMass * Stiffness;
BodyA.Position += normal * difference * BodyA.InverseMass;
BodyB.Position -= normal * difference * BodyB.InverseMass;
}
}
Angle Constraint
public class AngleConstraint
{
public VerletRigidBody BodyA { get; }
public VerletRigidBody BodyB { get; }
public VerletRigidBody BodyC { get; }
public float RestAngle { get; }
public float Stiffness { get; set; } = 1.0f;
public AngleConstraint(VerletRigidBody bodyA, VerletRigidBody bodyB, VerletRigidBody bodyC, float restAngle)
{
BodyA = bodyA;
BodyB = bodyB;
BodyC = bodyC;
RestAngle = restAngle;
}
public void Solve()
{
Vector2 ab = BodyA.Position - BodyB.Position;
Vector2 cb = BodyC.Position - BodyB.Position;
float abLength = ab.magnitude;
float cbLength = cb.magnitude;
if (abLength <= 0.0001f || cbLength <= 0.0001f)
return;
ab /= abLength;
cb /= cbLength;
float dot = Vector2.Dot(ab, cb);
dot = Math.Clamp(dot, -1.0f, 1.0f);
float angle = MathF.Acos(dot);
float diff = angle - RestAngle;
if (Math.Abs(diff) <= 0.0001f)
return;
// Calculate the direction to rotate
float cross = ab.x * cb.y - ab.y * cb.x;
float sign = cross < 0 ? -1.0f : 1.0f;
// Calculate the rotation amount
float rotationAmount = diff * Stiffness * sign;
// Apply rotation
RotatePoint(BodyA.Position, BodyB.Position, rotationAmount * BodyA.InverseMass);
RotatePoint(BodyC.Position, BodyB.Position, -rotationAmount * BodyC.InverseMass);
}
private void RotatePoint(Vector2 point, Vector2 center, float angle)
{
float cos = MathF.Cos(angle);
float sin = MathF.Sin(angle);
Vector2 direction = point - center;
point.x = center.x + direction.x * cos - direction.y * sin;
point.y = center.y + direction.x * sin + direction.y * cos;
}
}
Particle Systems
Particle systems simulate large numbers of small particles for effects like fire, smoke, or water.
Basic Particle System
public class Particle
{
public Vector2 Position { get; set; }
public Vector2 Velocity { get; set; }
public float LifeTime { get; set; }
public float MaxLifeTime { get; set; }
public float Size { get; set; }
public bool IsActive { get; set; }
public Particle()
{
IsActive = false;
}
public void Update(float deltaTime)
{
if (!IsActive)
return;
Position += Velocity * deltaTime;
LifeTime -= deltaTime;
if (LifeTime <= 0)
{
IsActive = false;
}
}
}
public class ParticleSystem
{
private Particle[] particles;
private Random random = new Random();
public Vector2 EmitterPosition { get; set; }
public float EmissionRate { get; set; } = 10.0f; // Particles per second
public float ParticleLifeTime { get; set; } = 2.0f;
public float ParticleSize { get; set; } = 0.5f;
public float EmissionRadius { get; set; } = 0.5f;
public float MinSpeed { get; set; } = 1.0f;
public float MaxSpeed { get; set; } = 3.0f;
private float emissionAccumulator = 0.0f;
public ParticleSystem(int maxParticles)
{
particles = new Particle[maxParticles];
for (int i = 0; i < maxParticles; i++)
{
particles[i] = new Particle();
}
}
public void Update(float deltaTime)
{
// Emit new particles
emissionAccumulator += EmissionRate * deltaTime;
while (emissionAccumulator >= 1.0f)
{
EmitParticle();
emissionAccumulator -= 1.0f;
}
// Update existing particles
foreach (var particle in particles)
{
particle.Update(deltaTime);
}
}
private void EmitParticle()
{
// Find an inactive particle
foreach (var particle in particles)
{
if (!particle.IsActive)
{
// Randomize position within emission radius
float angle = (float)(random.NextDouble() * 2 * Math.PI);
float distance = (float)(random.NextDouble() * EmissionRadius);
Vector2 offset = new Vector2(
MathF.Cos(angle) * distance,
MathF.Sin(angle) * distance
);
// Randomize velocity
angle = (float)(random.NextDouble() * 2 * Math.PI);
float speed = MinSpeed + (float)(random.NextDouble() * (MaxSpeed - MinSpeed));
Vector2 velocity = new Vector2(
MathF.Cos(angle) * speed,
MathF.Sin(angle) * speed
);
// Initialize particle
particle.Position = EmitterPosition + offset;
particle.Velocity = velocity;
particle.LifeTime = ParticleLifeTime;
particle.MaxLifeTime = ParticleLifeTime;
particle.Size = ParticleSize;
particle.IsActive = true;
return;
}
}
}
public IEnumerable<Particle> GetActiveParticles()
{
return particles.Where(p => p.IsActive);
}
}
Particle Collision
public class CollisionParticleSystem : ParticleSystem
{
private int[,] grid; // 0 = empty, 1 = obstacle
public CollisionParticleSystem(int maxParticles, int[,] grid) : base(maxParticles)
{
this.grid = grid;
}
public new void Update(float deltaTime)
{
// Emit new particles
// (Same as base class)
// Update particles with collision
foreach (var particle in GetActiveParticles())
{
Vector2 oldPosition = particle.Position;
// Update position
particle.Position += particle.Velocity * deltaTime;
// Check for collision with grid
int cellX = (int)particle.Position.x;
int cellY = (int)particle.Position.y;
if (cellX >= 0 && cellX < grid.GetLength(0) && cellY >= 0 && cellY < grid.GetLength(1))
{
if (grid[cellX, cellY] == 1) // Obstacle
{
// Simple bounce: reflect velocity and position
particle.Position = oldPosition;
// Determine which direction to reflect
int oldCellX = (int)oldPosition.x;
int oldCellY = (int)oldPosition.y;
if (oldCellX != cellX)
particle.Velocity.x = -particle.Velocity.x * 0.8f; // Damping
if (oldCellY != cellY)
particle.Velocity.y = -particle.Velocity.y * 0.8f; // Damping
}
}
// Update lifetime
particle.LifeTime -= deltaTime;
if (particle.LifeTime <= 0)
{
particle.IsActive = false;
}
}
}
}
Conclusion
Physics simulation is a complex but essential aspect of game development. By understanding the fundamental algorithms and techniques, you can create more realistic and engaging game experiences.
While many game engines provide built-in physics systems, knowing how these systems work under the hood allows you to customize behavior, optimize performance, and debug issues more effectively.
The implementations provided in this section are simplified for clarity but demonstrate the core concepts of game physics simulation. In practice, you might use a dedicated physics engine like Box2D, Bullet, or the built-in physics systems in Unity or Unreal Engine.