Files
INF6B/simulations/CubeCollision/Program.cs

550 lines
17 KiB
C#
Raw Blame History

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Media;
using System.Windows.Forms;
using Timer = System.Windows.Forms.Timer;
#nullable enable
namespace CubeCollisionSimulator
{
public class Cube
{
public float X { get; set; }
public float Y { get; set; }
public float VelocityX { get; set; }
public float VelocityY { get; set; }
public float Size { get; set; }
public float Mass { get; set; }
public Color Color { get; set; }
public bool IsDragging { get; set; }
public Cube(float x, float y, float size, float mass, Color color)
{
X = x;
Y = y;
Size = size;
Mass = mass;
Color = color;
VelocityX = 0;
VelocityY = 0;
IsDragging = false;
}
public RectangleF GetBounds()
{
return new RectangleF(X, Y, Size, Size);
}
public bool Contains(Point point)
{
return GetBounds().Contains(point);
}
}
public class SimulatorForm : Form
{
private readonly List<Cube> cubes;
private readonly Timer timer;
private Cube? draggedCube;
private Point lastMousePos;
// Physics parameters
private float gravity = 0.5f;
private float restitution = 0.8f;
private float friction = 0.98f;
private float forceMultiplier = 0.3f;
// Sound management
private DateTime lastSoundTime = DateTime.MinValue;
private const int MinSoundIntervalMs = 50; // Minimum time between sounds
private int soundCooldownCounter = 0;
private CheckBox soundEnabledCheckbox;
// UI Controls
private TrackBar? gravitySlider;
private TrackBar? restitutionSlider;
private TrackBar? frictionSlider;
private TrackBar? forceSlider;
private Label? gravityLabel;
private Label? restitutionLabel;
private Label? frictionLabel;
private Label? forceLabel;
private Button? addCubeButton;
private Button? clearButton;
private Panel? controlPanel;
public SimulatorForm()
{
this.Text = "2D Cube Collision Simulator";
this.Size = new Size(1200, 800);
this.DoubleBuffered = true;
this.BackColor = Color.FromArgb(30, 30, 30);
cubes = new List<Cube>();
soundEnabledCheckbox = new CheckBox();
// Add initial cubes
Random rand = new Random();
for (int i = 0; i < 5; i++)
{
cubes.Add(new Cube(
rand.Next(100, 800),
rand.Next(100, 400),
50 + rand.Next(30),
1 + rand.Next(5),
Color.FromArgb(rand.Next(100, 255), rand.Next(100, 255), rand.Next(100, 255))
));
}
SetupControls();
timer = new Timer();
timer.Interval = 16; // ~60 FPS
timer.Tick += Update;
timer.Start();
this.Paint += OnPaint;
this.MouseDown += OnMouseDown;
this.MouseMove += OnMouseMove;
this.MouseUp += OnMouseUp;
}
private void SetupControls()
{
controlPanel = new Panel
{
Dock = DockStyle.Right,
Width = 250,
BackColor = Color.FromArgb(45, 45, 45),
Padding = new Padding(10)
};
int yPos = 20;
// Sound toggle
soundEnabledCheckbox = new CheckBox
{
Text = "Enable Collision Sound",
Location = new Point(10, yPos),
Size = new Size(230, 25),
ForeColor = Color.White,
Checked = false
};
controlPanel.Controls.Add(soundEnabledCheckbox);
yPos += 40;
// Gravity control
gravityLabel = new Label
{
Text = $"Gravity: {gravity:F2}",
Location = new Point(10, yPos),
Size = new Size(230, 20),
ForeColor = Color.White
};
controlPanel.Controls.Add(gravityLabel);
yPos += 25;
gravitySlider = new TrackBar
{
Location = new Point(10, yPos),
Size = new Size(230, 45),
Minimum = 0,
Maximum = 100,
Value = (int)(gravity * 10),
TickFrequency = 10
};
gravitySlider.ValueChanged += (s, e) =>
{
gravity = gravitySlider?.Value / 10f ?? 0.5f;
if (gravityLabel != null)
gravityLabel.Text = $"Gravity: {gravity:F2}";
};
controlPanel.Controls.Add(gravitySlider);
yPos += 60;
// Restitution control
restitutionLabel = new Label
{
Text = $"Bounciness: {restitution:F2}",
Location = new Point(10, yPos),
Size = new Size(230, 20),
ForeColor = Color.White
};
controlPanel.Controls.Add(restitutionLabel);
yPos += 25;
restitutionSlider = new TrackBar
{
Location = new Point(10, yPos),
Size = new Size(230, 45),
Minimum = 0,
Maximum = 100,
Value = (int)(restitution * 100),
TickFrequency = 10
};
restitutionSlider.ValueChanged += (s, e) =>
{
restitution = restitutionSlider?.Value / 100f ?? 0.8f;
if (restitutionLabel != null)
restitutionLabel.Text = $"Bounciness: {restitution:F2}";
};
controlPanel.Controls.Add(restitutionSlider);
yPos += 60;
// Friction control
frictionLabel = new Label
{
Text = $"Friction: {friction:F2}",
Location = new Point(10, yPos),
Size = new Size(230, 20),
ForeColor = Color.White
};
controlPanel.Controls.Add(frictionLabel);
yPos += 25;
frictionSlider = new TrackBar
{
Location = new Point(10, yPos),
Size = new Size(230, 45),
Minimum = 90,
Maximum = 100,
Value = (int)(friction * 100),
TickFrequency = 1
};
frictionSlider.ValueChanged += (s, e) =>
{
friction = frictionSlider?.Value / 100f ?? 0.98f;
if (frictionLabel != null)
frictionLabel.Text = $"Friction: {friction:F2}";
};
controlPanel.Controls.Add(frictionSlider);
yPos += 60;
// Force control
forceLabel = new Label
{
Text = $"Drag Force: {forceMultiplier:F2}",
Location = new Point(10, yPos),
Size = new Size(230, 20),
ForeColor = Color.White
};
controlPanel.Controls.Add(forceLabel);
yPos += 25;
forceSlider = new TrackBar
{
Location = new Point(10, yPos),
Size = new Size(230, 45),
Minimum = 1,
Maximum = 50,
Value = (int)(forceMultiplier * 10),
TickFrequency = 5
};
forceSlider.ValueChanged += (s, e) =>
{
forceMultiplier = forceSlider?.Value / 10f ?? 0.3f;
if (forceLabel != null)
forceLabel.Text = $"Drag Force: {forceMultiplier:F2}";
};
controlPanel.Controls.Add(forceSlider);
yPos += 60;
// Add cube button
addCubeButton = new Button
{
Text = "Add Cube",
Location = new Point(10, yPos),
Size = new Size(230, 35),
BackColor = Color.FromArgb(70, 130, 180),
ForeColor = Color.White,
FlatStyle = FlatStyle.Flat
};
addCubeButton.Click += (s, e) => AddRandomCube();
controlPanel.Controls.Add(addCubeButton);
yPos += 45;
// Clear button
clearButton = new Button
{
Text = "Clear All",
Location = new Point(10, yPos),
Size = new Size(230, 35),
BackColor = Color.FromArgb(180, 70, 70),
ForeColor = Color.White,
FlatStyle = FlatStyle.Flat
};
clearButton.Click += (s, e) => cubes.Clear();
controlPanel.Controls.Add(clearButton);
yPos += 45;
// Instructions
Label instructionsLabel = new Label
{
Text = "Instructions:\n\n<> Click and drag cubes\n<> Throw them by dragging\n<> Watch them collide!\n<> Adjust physics sliders\n<> Add more cubes\n<> Toggle sound on/off",
Location = new Point(10, yPos + 20),
Size = new Size(230, 200),
ForeColor = Color.LightGray
};
controlPanel.Controls.Add(instructionsLabel);
this.Controls.Add(controlPanel);
}
private void AddRandomCube()
{
Random rand = new Random();
cubes.Add(new Cube(
rand.Next(100, this.ClientSize.Width - 350),
rand.Next(100, 300),
40 + rand.Next(40),
1 + rand.Next(5),
Color.FromArgb(rand.Next(100, 255), rand.Next(100, 255), rand.Next(100, 255))
));
}
private void PlayCollisionSound()
{
if (!soundEnabledCheckbox.Checked)
return;
// Cooldown to prevent too many sounds
if (soundCooldownCounter > 0)
{
soundCooldownCounter--;
return;
}
DateTime now = DateTime.Now;
if ((now - lastSoundTime).TotalMilliseconds < MinSoundIntervalMs)
return;
lastSoundTime = now;
soundCooldownCounter = 3; // Skip next 3 collision sounds
try
{
// Shorter, quieter beep
System.Threading.Tasks.Task.Run(() =>
{
try
{
Console.Beep(400, 30); // Lower frequency, shorter duration
}
catch { }
});
}
catch { }
}
private void OnMouseDown(object? sender, MouseEventArgs e)
{
if (e.X > this.ClientSize.Width - 250) return;
foreach (var cube in cubes.OrderByDescending(c => c.Size))
{
if (cube.Contains(e.Location))
{
draggedCube = cube;
draggedCube.IsDragging = true;
lastMousePos = e.Location;
break;
}
}
}
private void OnMouseMove(object? sender, MouseEventArgs e)
{
if (draggedCube != null && e.Button == MouseButtons.Left)
{
float dx = e.X - lastMousePos.X;
float dy = e.Y - lastMousePos.Y;
draggedCube.X += dx;
draggedCube.Y += dy;
draggedCube.VelocityX = dx * forceMultiplier;
draggedCube.VelocityY = dy * forceMultiplier;
lastMousePos = e.Location;
}
}
private void OnMouseUp(object? sender, MouseEventArgs e)
{
if (draggedCube != null)
{
draggedCube.IsDragging = false;
draggedCube = null;
}
}
private void Update(object? sender, EventArgs e)
{
// Optimized physics loop
int cubeCount = cubes.Count;
for (int i = 0; i < cubeCount; i++)
{
var cube = cubes[i];
if (!cube.IsDragging)
{
// Apply gravity
cube.VelocityY += gravity;
// Apply friction
cube.VelocityX *= friction;
cube.VelocityY *= friction;
// Update position
cube.X += cube.VelocityX;
cube.Y += cube.VelocityY;
// Wall collisions
int rightBoundary = this.ClientSize.Width - 250;
if (cube.X <= 0)
{
cube.X = 0;
cube.VelocityX = -cube.VelocityX * restitution;
PlayCollisionSound();
}
else if (cube.X + cube.Size >= rightBoundary)
{
cube.X = rightBoundary - cube.Size;
cube.VelocityX = -cube.VelocityX * restitution;
PlayCollisionSound();
}
if (cube.Y <= 0)
{
cube.Y = 0;
cube.VelocityY = -cube.VelocityY * restitution;
PlayCollisionSound();
}
else if (cube.Y + cube.Size >= this.ClientSize.Height)
{
cube.Y = this.ClientSize.Height - cube.Size;
cube.VelocityY = -cube.VelocityY * restitution;
PlayCollisionSound();
}
}
}
// Optimized cube-to-cube collisions
for (int i = 0; i < cubeCount; i++)
{
for (int j = i + 1; j < cubeCount; j++)
{
if (CheckCollision(cubes[i], cubes[j]))
{
ResolveCollision(cubes[i], cubes[j]);
PlayCollisionSound();
}
}
}
this.Invalidate();
}
private bool CheckCollision(Cube a, Cube b)
{
return a.GetBounds().IntersectsWith(b.GetBounds());
}
private void ResolveCollision(Cube a, Cube b)
{
// Calculate centers
float aCenterX = a.X + a.Size / 2;
float aCenterY = a.Y + a.Size / 2;
float bCenterX = b.X + b.Size / 2;
float bCenterY = b.Y + b.Size / 2;
// Collision normal
float dx = bCenterX - aCenterX;
float dy = bCenterY - aCenterY;
float distance = (float)Math.Sqrt(dx * dx + dy * dy);
if (distance == 0) return;
float nx = dx / distance;
float ny = dy / distance;
// Separate cubes
float overlap = (a.Size + b.Size) / 2 - distance;
if (overlap > 0)
{
float separationX = nx * overlap * 0.5f;
float separationY = ny * overlap * 0.5f;
a.X -= separationX;
a.Y -= separationY;
b.X += separationX;
b.Y += separationY;
}
// Relative velocity
float dvx = b.VelocityX - a.VelocityX;
float dvy = b.VelocityY - a.VelocityY;
// Velocity along normal
float velAlongNormal = dvx * nx + dvy * ny;
if (velAlongNormal > 0) return;
// Calculate impulse
float impulse = -(1 + restitution) * velAlongNormal;
impulse /= (1 / a.Mass + 1 / b.Mass);
// Apply impulse
float impulseX = impulse * nx;
float impulseY = impulse * ny;
a.VelocityX -= impulseX / a.Mass;
a.VelocityY -= impulseY / a.Mass;
b.VelocityX += impulseX / b.Mass;
b.VelocityY += impulseY / b.Mass;
}
private void OnPaint(object? sender, PaintEventArgs e)
{
Graphics g = e.Graphics;
g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
foreach (var cube in cubes)
{
using (SolidBrush brush = new SolidBrush(cube.Color))
{
g.FillRectangle(brush, cube.GetBounds());
}
if (cube.IsDragging)
{
using (Pen pen = new Pen(Color.Yellow, 3))
{
g.DrawRectangle(pen, cube.X, cube.Y, cube.Size, cube.Size);
}
}
// Draw mass indicator
using (Font font = new Font("Arial", 8))
using (SolidBrush textBrush = new SolidBrush(Color.White))
{
string massText = $"M:{cube.Mass}";
g.DrawString(massText, font, textBrush, cube.X + 5, cube.Y + 5);
}
}
}
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new SimulatorForm());
}
}
}