550 lines
17 KiB
C#
550 lines
17 KiB
C#
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());
|
||
}
|
||
}
|
||
} |