提问人:Schrödingers Capybara 提问时间:3/9/2023 最后编辑:Schrödingers Capybara 更新时间:3/20/2023 访问量:346
在 c# 模拟中添加球碰撞
Adding ball collisions in c# simulation
问:
我一直在通过制作一个基本的球弹跳模拟来学习 c#,有点像带有气泡的 Windows 屏幕保护程序。
我有两个球在屏幕上弹跳,但是当它们碰撞时它们消失了,我不知道为什么。
我使用“Console.WriteLine(value)”进行了调试,发现大多数值在碰撞后都等同于无穷大。
我最终放弃了该代码,但需要更好的球碰撞解决方案。
** 注意 ** 这并不总是只是两个球在屏幕上弹跳,这只是我试图学习碰撞 ** 注意 **
任何了解 Verlet 集成的人都会不胜感激,因为我非常困惑。
以下是我的一些代码和我正在使用的 C# 版本:
//+++ = I don't know what this is, a yt tutoriaol told me to use it
using System;
using System.Collections.Generic; //+++
using System.ComponentModel; //+++
using System.Data; //+++
using System.Drawing;
using System.Linq; //+++
using System.Text; //+++
using System.Threading.Tasks; //+++
using System.Windows.Forms; // This doesn't work in standard c#, only in mono for some reason.
public class Form1 : Form
{
float screenWidth;
float screenHeight;
float xpa = 0;
float ypa = 0;
float xva = 2;
float yva = 2;
float xpb; //later this is set to the width of the form minus the width of the ellipse this is marking the position of
float ypb; //later this is set to the height of the form, minus the height of the ellipse this is marking the position of
float xvb = -2;
float yvb = -2;
//...Unimportant code here...\\
var refreshTimer = new Timer();
refreshTimer.Interval = 1;
refreshTimer.Tick += new EventHandler(refreshTimer_Tick);
refreshTimer.Start();
}
//...Unimportant code here...\\
private void refreshTimer_Tick(object sender, EventArgs e)
{
this.Invalidate();
}
private void Form1_Paint(object sender, PaintEventArgs e)
{
Graphics g = e.Graphics;
//...Unimportant code here...\\
//Both ellipses bounce when in contact with the wall
//Ellipse A is located at coords (xpa, ypa) with width and height 50
//Ellipse A is located at coords (xpb, ypb) with width and height 50
//Collisions between A & B
float dx = (xpb + 25) - (xpa + 25);
float dy = (ypb + 25) - (ypa + 25);
float distance = (float)Math.Sqrt(dx * dx + dy * dy);
if (distance <= 50)
{
float angle = (float)Math.Atan2(dy, dx);
float sin = (float)Math.Sin(angle);
float cos = (float)Math.Cos(angle);
}
}
//...Rest of Code...\\
有谁知道 Verlet Integration 或任何其他可以帮助我的技术?
答:
我通过添加一个类并使用 System.Numerics 命名空间中的 Vector2 结构来简化代码(我在下面包含了 mono 的最小实现)。 包含用于向量数学的有用方法和运算符。例如,您可以使用 .Ball
Vector2
Vector2 result = v1 + v2
该类包装了球的所有状态和一些方法,如 .优点是,我们只需要为所有球编写一次此代码。 现在存储球的中心坐标,而不是左上角的位置。这使得推理变得更容易。它还存储半径,允许我们拥有不同半径的球。Ball
CollideWithWall
Ball
对于碰撞,我从用户 mmcdole 那里找到了一个有效的解决方案。我将其改编为 C# 和您的模拟。但是模拟的核心,即获得运动的速度的积分,保持不变。
public class Ball
{
public Brush Brush { get; set; }
public Vector2 Center { get; set; }
public Vector2 Velocity { get; set; }
public float Radius { get; set; }
// Make mass proportional to the area of the circle
public float Mass => Radius * Radius;
public void Move()
{
Center += Velocity;
}
public void CollideWithWall(Rectangle wall)
{
// Only reverse velocity if moving towards the walls
if (Center.X + Radius >= wall.Right && Velocity.X > 0 || Center.X - Radius < 0 && Velocity.X < 0) {
Velocity = new Vector2(-Velocity.X, Velocity.Y);
}
if (Center.Y + Radius >= wall.Bottom && Velocity.Y > 0 || Center.Y - Radius < 0 && Velocity.Y < 0) {
Velocity = new Vector2(Velocity.X, -Velocity.Y);
}
}
public void CollideWith(Ball other)
{
// From: https://stackoverflow.com/q/345838/880990, author: mmcdole
Vector2 delta = Center - other.Center;
float d = delta.Length();
if (d <= Radius + other.Radius && d > 1e-5) {
// Minimum translation distance to push balls apart after intersecting
Vector2 mtd = delta * ((Radius + other.Radius - d) / d);
// Resolve intersection - inverse mass quantities
float im1 = 1 / Mass;
float im2 = 1 / other.Mass;
// Push-pull them apart based off their mass
Center += mtd * (im1 / (im1 + im2));
other.Center -= mtd * (im2 / (im1 + im2));
// Impact speed
Vector2 v = Velocity - other.Velocity;
Vector2 mtdNormalized = Vector2.Normalize(mtd);
float vn = Vector2.Dot(v, mtdNormalized);
// Sphere intersecting but moving away from each other already
if (vn > 0.0f) return;
// Collision impulse
const float Restitution = 1.0f; // perfectly elastic collision
float i = -(1.0f + Restitution) * vn / (im1 + im2);
Vector2 impulse = mtdNormalized * i;
// Change in momentum
Velocity += impulse * im1;
other.Velocity -= impulse * im2;
}
}
public void Draw(Graphics g)
{
g.FillEllipse(Brush, Center.X - Radius, Center.Y - Radius, 2 * Radius, 2 * Radius);
}
}
然后我们可以用 (inForm1
)
Ball a = new Ball() {
Brush = Brushes.Red,
Center = new Vector2(),
Velocity = new Vector2(2, 2),
Radius = 25
};
Ball b = new Ball() {
Brush = Brushes.Blue,
Center = new Vector2(),
Velocity = new Vector2(-2, -2),
Radius = 40
};
public Form1()
{
InitializeComponent();
DoubleBuffered = true;
Load += Form1_Load; ;
Paint += Form1_Paint;
var refreshTimer = new System.Windows.Forms.Timer {
Interval = 1
};
refreshTimer.Tick += RefreshTimer_Tick;
refreshTimer.Start();
}
void Form1_Load(object sender, EventArgs e)
{
WindowState = FormWindowState.Normal;
System.Diagnostics.Debug.WriteLine(Width);
b.Center = new Vector2(Width - 60, Height - 60);
}
private void RefreshTimer_Tick(object sender, EventArgs e)
{
Invalidate();
}
我们的 Paint 方法现在如下所示:
private void Form1_Paint(object sender, PaintEventArgs e)
{
Graphics g = e.Graphics;
g.FillRectangle(Brushes.LightBlue, ClientRectangle);
a.Draw(g);
b.Draw(g);
a.Move();
b.Move();
a.CollideWithWall(ClientRectangle);
b.CollideWithWall(ClientRectangle);
a.CollideWith(b);
}
如果要在窗体设计器中更改窗体属性,则还必须调用窗体的构造函数。InitializeComponent();
编辑:
由于您使用的是没有结构的 mono,因此这里是它的最小版本,仅实现了上述代码所需的内容:Vextor2
public struct Vector2
{
public float X;
public float Y;
public Vector2(float x, float y)
{
X = x;
Y = y;
}
public static Vector2 operator +(Vector2 left, Vector2 right)
{
return new Vector2(left.X + right.X, left.Y + right.Y);
}
public static Vector2 operator -(Vector2 left, Vector2 right)
{
return new Vector2(left.X - right.X, left.Y - right.Y);
}
public static Vector2 operator *(Vector2 left, Vector2 right)
{
return new Vector2(left.X * right.X, left.Y * right.Y);
}
public static Vector2 operator *(float left, Vector2 right)
{
return new Vector2(left * right.X, left * right.Y);
}
public static Vector2 operator *(Vector2 left, float right)
{
return new Vector2(left.X * right, left.Y * right);
}
public static float Dot(Vector2 value1, Vector2 value2)
{
return value1.X * value2.X + value1.Y * value2.Y;
}
public static Vector2 Normalize(Vector2 value)
{
float d = MathF.Sqrt(value.X * value.X + value.Y * value.Y);
if (d < 1e-10) {
return value;
}
float invNorm = 1.0f / d;
return new Vector2(value.X * invNorm, value.Y * invNorm);
}
public float Length()
{
return MathF.Sqrt(X * X + Y * Y);
}
}
解释
我不打算解释碰撞本身。为此,请点击 mmcdole 代码的链接。
您正在使用很多变量,例如 、 、 、 、 。我所做的大多数更改都是为了减少变量的数量并避免代码重复。xpa
ypa
xva
yva
xpb
ypb
xvb
yvb
例如,我们有并存储对象的位置。该类型在其 和 字段中存储坐标,并且只需要一个变量。它还包含允许对它们执行算术运算的方法和运算符重载。float xpa
float ypa
a
Vector2
X
Y
例:
// Old code with individual floats
float xpa = 0;
float ypa = 0;
float xva = 2;
float yva = 2;
...
xpa += xva;
ypa += yva;
// New code with Vector2
Vector2 pa = new Vector2(0, 0);
Vector2 va = new Vector2(2, 2);
...
pa += va;
另一个问题是,很多代码是重复的,因为它必须应用于变量和变量。特别是在方法上。<whatever>a
<whatever>b
Form1_Paint
这个想法是将属于球的所有变量包装在一个对象中(声明为类)。在此对象中,变量(或带有 的属性)具有相同的名称,无论该对象是代表球还是球。Ball
{ get; set; }
a
b
此类中的方法现在使用对象的属性。Ball
例:
public void Draw(Graphics g)
{
g.FillEllipse(Brush, Center.X - Radius, Center.Y - Radius, 2 * Radius, 2 * Radius);
}
它使用对象的 、 和 属性。我决定将球的颜色存储为 ,因为需要刷子。Brush
Center
Radius
Brush
FillEllipse
从外面看,如果我们有两个球叫 和 ,我们可以用叫声来画它们:a
b
a.Draw(g);
b.Draw(g);
消除了一次代码重复!这同样适用于 、 和(与另一个球相撞)。Move
CollideWithWall
CollideWith
此代码的工作方式与您的代码类似,但球与球之间的碰撞除外。
另请参阅:
评论
Vector2
的源代码并复制相关部分,甚至可以创建自己的矢量类型。这比使用更好,因为它更具可读性,您可以添加一些逻辑。顺便说一句。 应该在 .NET 7.0 中工作。我在 .NET 7.0 / Windows 11 中运行上面的示例float[]
System.Windows.Forms
mono
MathF.Sqrt
这是一个通用的解决方案,它为我的第一个答案添加了一些缺失的东西。
它添加了两个新方法:Vector2
public float LengthSquared()
{
return X * X + Y * Y;
}
public override string ToString() => $"Vector2({X:n3}, {Y:n3})";
LengthSquared
将用于计算空气阻力。覆盖可简化调试。ToString
我用一个球阵列替换了两个不同的球场,使我们能够轻松地将更多球添加到游戏中。
private readonly Ball[] _balls = new Ball[]{
new Ball() {
Brush = Brushes.Red,
Center = new Vector2(100, 135),
Velocity = new Vector2(10, 0),
Radius = 40
},
new Ball() {
Brush = Brushes.Blue,
Center = new Vector2(300, 135.1f),
Velocity = new Vector2(-4, 0),
Radius = 15
},
new Ball() {
Brush = Brushes.Green,
Center = new Vector2(30, 20),
Velocity = new Vector2(5, 3),
Radius = 20
}
};
Form1_Paint
现在可以在一个循环中处理所有球。这是对第一种解决方案的简化,在第一个解决方案中,我们必须单独处理每个球。我们还增加了地面上的摩擦力、空气阻力和重力。为此,我们以以下形式声明其他字段和常量:
private const float C = 0.1f; // Air drag coefficient (0 = no drag)
private const float Friction = 0.95f; // Friction coefficient (1 = no friction)
private readonly Vector2 _gravity = new Vector2(0, 0.1f);
涂装方法:
private void Form1_Paint(object sender, PaintEventArgs e)
{
Graphics g = e.Graphics;
g.Clear(Color.LightBlue);
for (int i = 0; i < _balls.Length; i++) {
Ball ball = _balls[i];
ball.Draw(g);
if (ball.Center.Y + ball.Radius >= ClientRectangle.Bottom) {
// On ground. Friction with ground
ball.Velocity *= Friction;
} else {
// Accelerate downwards only when not on ground.
ball.Velocity += _gravity;
}
// Air Drag
float v2 = ball.Velocity.LengthSquared();
float vAbs = MathF.Sqrt(v2);
float fDrag = C * v2;
Vector2 f = fDrag / vAbs * ball.Velocity;
ball.Velocity -= 1 / ball.Mass * f;
ball.CollideWithWall(ClientRectangle);
// Start at i + 1 to handle collision of each pair of balls only once.
for (int j = i + 1; j < _balls.Length; j++) {
ball.CollideWith(_balls[j]);
}
ball.Move();
}
}
我还在与墙壁碰撞时添加了一点阻尼(在课堂上):Ball
public void CollideWithWall(Rectangle wall)
{
const float damping = 0.99f; // 1 = no damping
// Only reverse velocity if moving towards the walls
if (Center.X + Radius >= wall.Right && Velocity.X > 0 || Center.X - Radius < 0 && Velocity.X < 0) {
Velocity = new Vector2(-damping * Velocity.X, Velocity.Y);
}
if (Center.Y + Radius >= wall.Bottom && Velocity.Y > 0 || Center.Y - Radius < 0 && Velocity.Y < 0) {
Velocity = new Vector2(Velocity.X, -damping * Velocity.Y);
}
}
现在,由于空气阻力、与地面的摩擦以及从墙上弹起时的阻尼,球会在一段时间后静止下来。
评论
elasticCollision
va[0] + vb[0]
Timer
Application.Idle