在 c# 模拟中添加球碰撞

Adding ball collisions in c# simulation

提问人:Schrödingers Capybara 提问时间:3/9/2023 最后编辑:Schrödingers Capybara 更新时间:3/20/2023 访问量:346

问:

我一直在通过制作一个基本的球弹跳模拟来学习 c#,有点像带有气泡的 Windows 屏幕保护程序。
我有两个球在屏幕上弹跳,但是当它们碰撞时它们消失了,我不知道为什么。

我使用“Console.WriteLine(value)”进行了调试,发现大多数值在碰撞后都等同于无穷大。

我最终放弃了该代码,但需要更好的球碰撞解决方案。

** 注意 ** 这并不总是只是两个球在屏幕上弹跳,这只是我试图学习碰撞 ** 注意 **

任何了解 Verlet 集成的人都会不胜感激,因为我非常困惑。

以下是我的一些代码和我正在使用的 C# 版本:

显示 c# 版本的 replit 的屏幕截图

//+++ = 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 或任何其他可以帮助我的技术?

C# 单声道 游戏-物理 verlet 集成

评论

0赞 phuzi 3/9/2023
在你除以的方法中,如果这是零,你会得到一个除以零的异常,如果它非常小,那么产生的速度将非常大。尝试调试它,看看你得到了什么值。elasticCollisionva[0] + vb[0]
0赞 Schrödingers Capybara 3/9/2023
控制台.WriteLine(vFinalA[0]);无限 Console.WriteLine(vFinalB[0]);无限
0赞 John Alexiou 3/14/2023
仅供参考 - 考虑放弃并使用 实现游戏循环。您将获得更高的帧速率,从而使动画更加流畅。TimerApplication.Idle
0赞 Schrödingers Capybara 3/14/2023
我像现在这样使用计时器,这样我就可以控制帧速率,因为我不希望它移动得过快。
0赞 John Alexiou 3/14/2023
请注意,verlett 集成与冲突逻辑无关。

答:

2赞 Olivier Jacot-Descombes 3/9/2023 #1

我通过添加一个类并使用 System.Numerics 命名空间中的 Vector2 结构来简化代码(我在下面包含了 mono 的最小实现)。 包含用于向量数学的有用方法和运算符。例如,您可以使用 .BallVector2Vector2 result = v1 + v2

该类包装了球的所有状态和一些方法,如 .优点是,我们只需要为所有球编写一次此代码。 现在存储球的中心坐标,而不是左上角的位置。这使得推理变得更容易。它还存储半径,允许我们拥有不同半径的球。BallCollideWithWallBall

对于碰撞,我从用户 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 代码的链接。

您正在使用很多变量,例如 、 、 、 、 。我所做的大多数更改都是为了减少变量的数量并避免代码重复。xpaypaxvayvaxpbypbxvbyvb

例如,我们有并存储对象的位置。该类型在其 和 字段中存储坐标,并且只需要一个变量。它还包含允许对它们执行算术运算的方法和运算符重载。float xpafloat ypaaVector2XY

例:

// 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>bForm1_Paint

这个想法是将属于球的所有变量包装在一个对象中(声明为类)。在此对象中,变量(或带有 的属性)具有相同的名称,无论该对象是代表球还是球。Ball{ get; set; }ab

此类中的方法现在使用对象的属性。Ball

例:

public void Draw(Graphics g)
{
    g.FillEllipse(Brush, Center.X - Radius, Center.Y - Radius, 2 * Radius, 2 * Radius);
}

它使用对象的 、 和 属性。我决定将球的颜色存储为 ,因为需要刷子。BrushCenterRadiusBrushFillEllipse

从外面看,如果我们有两个球叫 和 ,我们可以用叫声来画它们:ab

a.Draw(g);
b.Draw(g);

消除了一次代码重复!这同样适用于 、 和(与另一个球相撞)。MoveCollideWithWallCollideWith

此代码的工作方式与您的代码类似,但球与球之间的碰撞除外。

另请参阅:

评论

0赞 Schrödingers Capybara 3/9/2023
墙壁碰撞工作正常,我只需要让球与球碰撞工作。但是感谢您的这段代码,我将使用它。问题是,当两个球接触时,有些东西会破裂,它们会以无穷大的速度反射。
0赞 Schrödingers Capybara 3/11/2023
这是行不通的,因为在 Mono System.Numerics 中不起作用。
0赞 Olivier Jacot-Descombes 3/11/2023
您可以在此处查看 Vector2 的源代码并复制相关部分,甚至可以创建自己的矢量类型。这比使用更好,因为它更具可读性,您可以添加一些逻辑。顺便说一句。 应该在 .NET 7.0 中工作。我在 .NET 7.0 / Windows 11 中运行上面的示例float[]System.Windows.Forms
0赞 Schrödingers Capybara 3/11/2023
我不确定它是否是 .NET,我使用的是 replit 的 C#。如果可能的话,我会在问题中张贴一张图片。
0赞 Olivier Jacot-Descombes 3/11/2023
好的,由于您使用的是 mono(我在您的问题中添加了标签),我将我的解决方案降级为 C# 6.0 / .NET 4.5.2,并且还添加了此处未包含的功能,但 mono 具有它。这现在应该适用于单声道。monoMathF.Sqrt
1赞 Olivier Jacot-Descombes 3/15/2023 #2

这是一个通用的解决方案,它为我的第一个答案添加了一些缺失的东西。

它添加了两个新方法: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);
    }
}

现在,由于空气阻力、与地面的摩擦以及从墙上弹起时的阻尼,球会在一段时间后静止下来。