743 lines
24 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|
|
} |