Files
INF6B/simulations/balls/BallCollider/Form1.cs

743 lines
24 KiB
C#

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Media;
using System.Windows.Forms;
using WinTimer = System.Windows.Forms.Timer;
namespace BallCollider
{
public partial class Form1 : Form
{
// Ball properties
private PointF ballPos;
private PointF ballVel;
private const float BallRadius = 12f;
private const float MinSpeed = 6f;
private const float MaxSpeed = 20f;
// Shape properties
private List<List<PointF>> shapes;
private List<Color> shapeColors;
private const int MinWalls = 5;
private const int MaxWalls = 220;
private const float BaseShapeRadius = 220f;
private PointF shapeCenter;
// Game mode
private bool multiShapeMode = false;
private int currentShapeIndex = 0;
// Rendering optimization
private BufferedGraphicsContext context;
private BufferedGraphics buffer;
private readonly SolidBrush ballBrush;
private readonly List<Pen> shapePens;
// Game loop
private readonly WinTimer gameTimer;
private readonly Random rnd;
// Collision cooldown to prevent multiple hits
private int collisionCooldown = 0;
// Tracer system
private List<Tracer> tracers;
private const int MaxTracers = 80;
private int tracerCooldown = 0;
private const int TracerInterval = 2;
// Predictive collision detection
private PointF predictedPos;
private bool showPrediction = false;
public Form1()
{
InitializeComponent();
// Optimize form for rendering
this.DoubleBuffered = true;
this.SetStyle(ControlStyles.AllPaintingInWmPaint |
ControlStyles.UserPaint |
ControlStyles.Opaque, true);
this.ClientSize = new Size(800, 600);
this.Text = "balls";
this.BackColor = Color.Black;
rnd = new Random();
ballBrush = new SolidBrush(Color.White);
shapePens = new List<Pen>();
tracers = new List<Tracer>();
// Initialize buffered graphics
context = BufferedGraphicsManager.Current;
context.MaximumBuffer = new Size(this.Width + 1, this.Height + 1);
buffer = context.Allocate(this.CreateGraphics(), this.ClientRectangle);
shapeCenter = new PointF(ClientSize.Width / 2f, ClientSize.Height / 2f);
InitializeGame();
// High-frequency timer for smooth gameplay
gameTimer = new WinTimer();
gameTimer.Interval = 20; // ~60 FPS
gameTimer.Tick += GameLoop;
gameTimer.Start();
this.Resize += (s, e) => RecreateBuffer();
this.KeyDown += Form1_KeyDown;
this.KeyPreview = true;
}
private void Form1_KeyDown(object sender, KeyEventArgs e)
{
if (e.KeyCode == Keys.M)
{
multiShapeMode = !multiShapeMode;
if (multiShapeMode)
{
InitializeMultiShapeMode();
}
else
{
InitializeSingleShapeMode();
}
}
else if (e.KeyCode == Keys.P)
{
showPrediction = !showPrediction;
}
}
private void RecreateBuffer()
{
if (this.Width > 0 && this.Height > 0)
{
buffer?.Dispose();
context.MaximumBuffer = new Size(this.Width + 1, this.Height + 1);
buffer = context.Allocate(this.CreateGraphics(), this.ClientRectangle);
shapeCenter = new PointF(ClientSize.Width / 2f, ClientSize.Height / 2f);
}
}
private void InitializeGame()
{
shapes = new List<List<PointF>>();
shapeColors = new List<Color>();
shapePens.Clear();
tracers.Clear();
CreateShape(MinWalls, 0);
// Spawn ball at center
ballPos = new PointF(shapeCenter.X, shapeCenter.Y);
// Random initial velocity
float angle = (float)(rnd.NextDouble() * Math.PI * 2);
float speed = MinSpeed * 1.5f;
ballVel = new PointF(
(float)Math.Cos(angle) * speed,
(float)Math.Sin(angle) * speed
);
}
private void InitializeSingleShapeMode()
{
shapes.Clear();
shapeColors.Clear();
shapePens.Clear();
tracers.Clear();
CreateShape(MinWalls, 0);
currentShapeIndex = 0;
}
private void InitializeMultiShapeMode()
{
shapes.Clear();
shapeColors.Clear();
shapePens.Clear();
tracers.Clear();
// Create 3 concentric shapes
for (int i = 0; i < 3; i++)
{
CreateShape(MinWalls + i * 2, i);
}
currentShapeIndex = 0;
}
private void CreateShape(int sides, int shapeIndex)
{
while (shapes.Count <= shapeIndex)
{
shapes.Add(new List<PointF>());
shapeColors.Add(Color.White);
shapePens.Add(new Pen(Color.White, 4f));
}
List<PointF> shapePoints = new List<PointF>();
float angleStep = (float)(2 * Math.PI / sides);
float startAngle = (float)(rnd.NextDouble() * Math.PI * 2);
float radius = multiShapeMode ?
BaseShapeRadius * (1.0f - shapeIndex * 0.2f) :
BaseShapeRadius;
for (int i = 0; i < sides; i++)
{
float angle = startAngle + i * angleStep;
shapePoints.Add(new PointF(
shapeCenter.X + (float)Math.Cos(angle) * radius,
shapeCenter.Y + (float)Math.Sin(angle) * radius
));
}
shapes[shapeIndex] = shapePoints;
Color newColor = Color.FromArgb(
rnd.Next(100, 256),
rnd.Next(100, 256),
rnd.Next(100, 256)
);
shapeColors[shapeIndex] = newColor;
shapePens[shapeIndex].Color = newColor;
}
private void GameLoop(object sender, EventArgs e)
{
UpdateBall();
UpdateTracers();
Render();
}
private void UpdateBall()
{
if (collisionCooldown > 0)
collisionCooldown--;
// Store previous position for tracer
PointF previousPos = ballPos;
predictedPos = new PointF(ballPos.X + ballVel.X, ballPos.Y + ballVel.Y);
if (collisionCooldown == 0)
{
if (CheckPredictiveCollision())
{
// Predictive collision detected - handle it immediately
HandlePredictiveCollision();
}
else
{
// No collision predicted - apply normal movement
ballPos = predictedPos;
}
}
else
{
// Apply normal movement during cooldown
ballPos = predictedPos;
}
// Gradually increase speed for excitement (capped)
float speed = (float)Math.Sqrt(ballVel.X * ballVel.X + ballVel.Y * ballVel.Y);
if (speed < MaxSpeed)
{
float factor = 1.005f;
ballVel.X *= factor;
ballVel.Y *= factor;
}
// Add tracer
if (tracerCooldown <= 0)
{
AddTracer(previousPos);
tracerCooldown = TracerInterval;
}
else
{
tracerCooldown--;
}
// Final boundary enforcement (safety net)
EnforceBoundaries();
}
private bool CheckPredictiveCollision()
{
if (shapes.Count == 0) return false;
List<PointF> currentShape = shapes[currentShapeIndex];
for (int i = 0; i < currentShape.Count; i++)
{
PointF p1 = currentShape[i];
PointF p2 = currentShape[(i + 1) % currentShape.Count];
PointF closest = ClosestPointOnSegment(p1, p2, predictedPos);
float dx = predictedPos.X - closest.X;
float dy = predictedPos.Y - closest.Y;
float dist = (float)Math.Sqrt(dx * dx + dy * dy);
if (dist < BallRadius)
{
return true;
}
}
return false;
}
private void HandlePredictiveCollision()
{
if (shapes.Count == 0) return;
List<PointF> currentShape = shapes[currentShapeIndex];
PointF collisionPoint = PointF.Empty;
PointF wallNormal = PointF.Empty;
int wallIndex = -1;
float minDist = float.MaxValue;
// Find the closest wall that will be collided with
for (int i = 0; i < currentShape.Count; i++)
{
PointF p1 = currentShape[i];
PointF p2 = currentShape[(i + 1) % currentShape.Count];
PointF closest = ClosestPointOnSegment(p1, p2, predictedPos);
float dx = predictedPos.X - closest.X;
float dy = predictedPos.Y - closest.Y;
float dist = (float)Math.Sqrt(dx * dx + dy * dy);
if (dist < BallRadius && dist < minDist)
{
minDist = dist;
collisionPoint = closest;
wallIndex = i;
// Calculate wall normal
float edgeX = p2.X - p1.X;
float edgeY = p2.Y - p1.Y;
float edgeLen = (float)Math.Sqrt(edgeX * edgeX + edgeY * edgeY);
if (edgeLen > 0)
{
wallNormal.X = -edgeY / edgeLen;
wallNormal.Y = edgeX / edgeLen;
// Ensure normal points inward
float midX = (p1.X + p2.X) / 2f;
float midY = (p1.Y + p2.Y) / 2f;
float toCenterX = shapeCenter.X - midX;
float toCenterY = shapeCenter.Y - midY;
if (wallNormal.X * toCenterX + wallNormal.Y * toCenterY < 0)
{
wallNormal.X = -wallNormal.X;
wallNormal.Y = -wallNormal.Y;
}
}
}
}
if (wallIndex >= 0 && wallNormal != PointF.Empty)
{
// Calculate exact collision position (ball surface touching wall)
float penetration = BallRadius - minDist;
PointF correctedPos = new PointF(
predictedPos.X + wallNormal.X * penetration,
predictedPos.Y + wallNormal.Y * penetration
);
// Set ball to corrected position
ballPos = correctedPos;
// Reflect velocity
float dotProduct = ballVel.X * wallNormal.X + ballVel.Y * wallNormal.Y;
if (dotProduct < 0)
{
ballVel.X -= 2 * dotProduct * wallNormal.X;
ballVel.Y -= 2 * dotProduct * wallNormal.Y;
// Apply boost and clamp speed
float boost = 1.05f;
ballVel.X *= boost;
ballVel.Y *= boost;
float currentSpeed = (float)Math.Sqrt(ballVel.X * ballVel.X + ballVel.Y * ballVel.Y);
if (currentSpeed > MaxSpeed)
{
ballVel.X = (ballVel.X / currentSpeed) * MaxSpeed;
ballVel.Y = (ballVel.Y / currentSpeed) * MaxSpeed;
}
PlayHitSound();
// Handle shape evolution
PointF p1 = currentShape[wallIndex];
PointF p2 = currentShape[(wallIndex + 1) % currentShape.Count];
if (currentShape.Count < MaxWalls)
{
// Add vertex to make shape more circular
PointF newPoint = new PointF(
(p1.X + p2.X) / 2f,
(p1.Y + p2.Y) / 2f
);
float radius = multiShapeMode ?
BaseShapeRadius * (1.0f - currentShapeIndex * 0.2f) :
BaseShapeRadius;
// Project to maintain circular shape
float angle = (float)Math.Atan2(newPoint.Y - shapeCenter.Y, newPoint.X - shapeCenter.X);
newPoint = new PointF(
shapeCenter.X + (float)Math.Cos(angle) * radius,
shapeCenter.Y + (float)Math.Sin(angle) * radius
);
currentShape.Insert(wallIndex + 1, newPoint);
}
else
{
if (multiShapeMode && currentShapeIndex < shapes.Count - 1)
{
currentShapeIndex++;
PlayShapeChangeSound();
}
else
{
int sides = multiShapeMode ? MinWalls + currentShapeIndex * 2 : MinWalls;
CreateShape(sides, currentShapeIndex);
PlayShapeChangeSound();
}
}
collisionCooldown = 8;
}
}
}
private void EnforceBoundaries()
{
bool isInsideAnyShape = false;
foreach (var shape in shapes)
{
if (IsPointInPolygon(ballPos, shape))
{
isInsideAnyShape = true;
break;
}
}
if (!isInsideAnyShape && shapes.Count > 0)
{
PointF closestBoundary = FindClosestBoundaryPoint(ballPos, shapes[0]);
float pushX = closestBoundary.X - ballPos.X;
float pushY = closestBoundary.Y - ballPos.Y;
float pushDist = (float)Math.Sqrt(pushX * pushX + pushY * pushY);
if (pushDist > 0)
{
float margin = BallRadius + 2f;
ballPos.X = closestBoundary.X - (pushX / pushDist) * margin;
ballPos.Y = closestBoundary.Y - (pushY / pushDist) * margin;
// Emergency bounce
float randomAngle = (float)(rnd.NextDouble() * Math.PI * 2);
float currentSpeed = (float)Math.Sqrt(ballVel.X * ballVel.X + ballVel.Y * ballVel.Y);
ballVel.X = (float)Math.Cos(randomAngle) * currentSpeed;
ballVel.Y = (float)Math.Sin(randomAngle) * currentSpeed;
}
}
}
private void UpdateTracers()
{
// Update and remove old tracers
for (int i = tracers.Count - 1; i >= 0; i--)
{
tracers[i].Update();
if (tracers[i].IsDead)
{
tracers.RemoveAt(i);
}
}
}
private void AddTracer(PointF position)
{
if (tracers.Count >= MaxTracers)
{
tracers.RemoveAt(0);
}
Color tracerColor = shapes.Count > 0 ?
Color.FromArgb(150, shapeColors[currentShapeIndex]) :
Color.FromArgb(150, Color.White);
tracers.Add(new Tracer(position, tracerColor, BallRadius * 0.7f));
}
private PointF FindClosestBoundaryPoint(PointF point, List<PointF> shape)
{
PointF closest = shape[0];
float closestDist = float.MaxValue;
for (int i = 0; i < shape.Count; i++)
{
PointF p1 = shape[i];
PointF p2 = shape[(i + 1) % shape.Count];
PointF segmentClosest = ClosestPointOnSegment(p1, p2, point);
float dist = Distance(point, segmentClosest);
if (dist < closestDist)
{
closestDist = dist;
closest = segmentClosest;
}
}
return closest;
}
private float Distance(PointF a, PointF b)
{
float dx = a.X - b.X;
float dy = a.Y - b.Y;
return (float)Math.Sqrt(dx * dx + dy * dy);
}
private bool IsPointInPolygon(PointF point, List<PointF> polygon)
{
bool inside = false;
int j = polygon.Count - 1;
for (int i = 0; i < polygon.Count; i++)
{
if ((polygon[i].Y > point.Y) != (polygon[j].Y > point.Y) &&
point.X < (polygon[j].X - polygon[i].X) * (point.Y - polygon[i].Y) /
(polygon[j].Y - polygon[i].Y) + polygon[i].X)
{
inside = !inside;
}
j = i;
}
return inside;
}
private PointF ClosestPointOnSegment(PointF a, PointF b, PointF p)
{
float dx = b.X - a.X;
float dy = b.Y - a.Y;
if (dx == 0 && dy == 0)
return a;
float len2 = dx * dx + dy * dy;
float t = ((p.X - a.X) * dx + (p.Y - a.Y) * dy) / len2;
t = Math.Max(0, Math.Min(1, t));
return new PointF(a.X + t * dx, a.Y + t * dy);
}
private void PlayHitSound()
{
try
{
int freq = rnd.Next(400, 1000);
System.Threading.Tasks.Task.Run(() => Console.Beep(freq, 30));
}
catch { }
}
private void PlayShapeChangeSound()
{
try
{
System.Threading.Tasks.Task.Run(() =>
{
Console.Beep(300, 80);
System.Threading.Thread.Sleep(50);
Console.Beep(500, 80);
});
}
catch { }
}
private void Render()
{
Graphics g = buffer.Graphics;
g.SmoothingMode = SmoothingMode.AntiAlias;
g.Clear(Color.Black);
// Draw tracers first (behind everything)
foreach (var tracer in tracers)
{
tracer.Draw(g);
}
// Draw all shapes
for (int shapeIndex = 0; shapeIndex < shapes.Count; shapeIndex++)
{
var shape = shapes[shapeIndex];
if (shape.Count >= 2)
{
Color drawColor = shapeColors[shapeIndex];
if (multiShapeMode && shapeIndex == currentShapeIndex)
{
drawColor = Color.FromArgb(255,
Math.Min(255, drawColor.R + 50),
Math.Min(255, drawColor.G + 50),
Math.Min(255, drawColor.B + 50));
}
using (Pen pen = new Pen(drawColor, shapeIndex == currentShapeIndex ? 5f : 3f))
{
for (int i = 0; i < shape.Count; i++)
{
PointF p1 = shape[i];
PointF p2 = shape[(i + 1) % shape.Count];
g.DrawLine(pen, p1, p2);
}
}
}
}
// Draw prediction line if enabled
if (showPrediction)
{
using (Pen predictionPen = new Pen(Color.FromArgb(100, Color.Yellow), 1f))
{
g.DrawLine(predictionPen, ballPos, predictedPos);
}
// Draw predicted position
using (Brush predictionBrush = new SolidBrush(Color.FromArgb(100, Color.Red)))
{
g.FillEllipse(predictionBrush,
predictedPos.X - BallRadius / 2,
predictedPos.Y - BallRadius / 2,
BallRadius, BallRadius);
}
}
// Draw ball with glow effect
using (GraphicsPath path = new GraphicsPath())
{
path.AddEllipse(
ballPos.X - BallRadius,
ballPos.Y - BallRadius,
BallRadius * 2,
BallRadius * 2
);
using (PathGradientBrush brush = new PathGradientBrush(path))
{
Color activeColor = shapes.Count > 0 ? shapeColors[currentShapeIndex] : Color.White;
brush.CenterColor = Color.White;
brush.SurroundColors = new[] { Color.FromArgb(100, activeColor) };
g.FillPath(brush, path);
}
}
// Draw UI info
using (Font font = new Font("Arial", 14, FontStyle.Bold))
{
string sidesText = $"Sides: {shapes[currentShapeIndex].Count}";
string predictionText = $"Prediction: {(showPrediction ? "ON" : "OFF")} (P)";
g.DrawString(sidesText, font, Brushes.White, 10, 10);
g.DrawString(predictionText, font, Brushes.LightGreen, 10, 35);
}
buffer.Render();
}
protected override void OnPaint(PaintEventArgs e)
{
if (buffer != null)
{
buffer.Render(e.Graphics);
}
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
gameTimer?.Stop();
gameTimer?.Dispose();
buffer?.Dispose();
context?.Dispose();
ballBrush?.Dispose();
foreach (var pen in shapePens)
pen?.Dispose();
components?.Dispose();
}
base.Dispose(disposing);
}
}
// Tracer class for ball trail effect
public class Tracer
{
public PointF Position { get; private set; }
public Color Color { get; private set; }
public float Radius { get; private set; }
public float Life { get; private set; }
public bool IsDead => Life <= 0;
private readonly float initialRadius;
private readonly Color initialColor;
public Tracer(PointF position, Color color, float radius)
{
Position = position;
Color = color;
Radius = radius;
initialRadius = radius;
initialColor = color;
Life = 1.0f; // Start with full life
}
public void Update()
{
Life -= 0.03f; // Fade speed
if (Life < 0) Life = 0;
// Shrink and fade
Radius = initialRadius * Life;
// Fade color
Color = Color.FromArgb(
(int)(initialColor.A * Life),
initialColor.R,
initialColor.G,
initialColor.B
);
}
public void Draw(Graphics g)
{
if (IsDead) return;
using (Brush brush = new SolidBrush(Color))
{
g.FillEllipse(brush,
Position.X - Radius,
Position.Y - Radius,
Radius * 2,
Radius * 2);
}
}
}
}