Изменение координат шарика при помощи потоков

Есть некий шарик, который должен изменять свои координаты двигаясь по форме, используя потоки. Как правильно это реализовать?

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;
using System.Threading;

namespace WindowsFormsApp1
{
    volatile static int x1, y1;
    SolidBrush b = new SolidBrush(Color.Blue);
    SolidBrush r = new SolidBrush(Color.Red);
    static Random rnd = new Random();
    Thread t = new Thread(new ThreadStart (Write));
    
    public Form1()
    {
        InitializeComponent();           
    }
    
    private void Form1_Load(object sender, EventArgs e)
    {            
        t.Start();
    }
    
    public static void Write()
    {
        while (true)
        {
            x1 += rnd.Next(-50, 50);
            y1 += rnd.Next(-50, 50);
            if ((x1 >= 300) || (y1 >= 300) || (x1 <= 0) || (y1 <= 0))
            {
                x1 = rnd.Next(0, 300);
                y1 = rnd.Next(0, 300);
            }
            Thread.Sleep(TimeSpan.FromSeconds(2));
        }
    }
    
    private void Form1_Paint(object sender, PaintEventArgs e)
    {
        Graphics g = e.Graphics;
        g.FillEllipse(b, x1, y1, 10, 10);
    }

Ответы (1 шт):

Автор решения: Alexander Petrov

Для хранения координат одного шарика вы используете две переменные: x1, y1. А если вам ещё и цвет шарика нужно хранить? А если ещё и размер?..
Более того, вам нужен не один шарик, а множество. Как хранить все эти данные о них? Можно, конечно, сделать несколько коллекций:

List<int> xs;
List<int> ys;

И так по коллекции на каждый параметр шарика. Неудобно, не правда ли?

Я вам в прошлом ответе уже показывал пример с использованием следующего класса:

class Ball
{
    public int X { get; set; }
    public int Y { get; set; }
    public Brush Brush { get; set; }
}

Привыкайте думать объектами. Учитесь мыслить в моделях предметной области. У нас есть шарик (Ball), у него набор характеристик: координаты X и Y, цвет (в данном случае кисть) и прочие. Их легко добавить в этот класс и легко получать к ним доступ централизованно.


Рабочий пример приложения.

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Threading;
using System.Windows.Forms;

namespace WinForm
{
    public partial class Form1 : Form
    {
        private List<Ball> balls = new List<Ball>();
        private List<Thread> threads;

        public Form1()
        {
            //InitializeComponent();
            this.Load += Form1_Load;
            this.Paint += Form1_Paint;
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            Random rnd = new Random();

            balls.Add(new Ball { X = rnd.Next(0, 300), Y = rnd.Next(0, 300), Brush = Brushes.Red });
            balls.Add(new Ball { X = rnd.Next(0, 300), Y = rnd.Next(0, 300), Brush = Brushes.Green });
            balls.Add(new Ball { X = rnd.Next(0, 300), Y = rnd.Next(0, 300), Brush = Brushes.Blue });

            threads = new List<Thread>(balls.Count);

            for (int i = 0; i < balls.Count; i++)
            {
                var thread = new Thread(new ParameterizedThreadStart(MoveBall));
                threads.Add(thread);
                thread.IsBackground = true;                
                thread.Start(balls[i]);
            }
        }

        private void Form1_Paint(object sender, PaintEventArgs e)
        {
            foreach (var ball in balls)
            {
                e.Graphics.FillEllipse(ball.Brush, ball.X, ball.Y, 10, 10);
            }
        }

        private void MoveBall(object obj)
        {
            Ball ball = (Ball)obj;
            Random rnd = new Random(ball.GetHashCode());

            while (true)
            {
                //Invalidate(new Rectangle(ball.X, ball.Y, 10, 10));

                ball.X += rnd.Next(-10, 10);
                ball.Y += rnd.Next(-10, 10);

                if ((ball.X >= 300) || (ball.Y >= 300) || (ball.X <= 0) || (ball.Y <= 0))
                {
                    ball.X = rnd.Next(0, 300);
                    ball.Y = rnd.Next(0, 300);
                }

                Invalidate();
                //Invalidate(new Rectangle(ball.X, ball.Y, 10, 10));

                Thread.Sleep(TimeSpan.FromMilliseconds(300));
            }
        }
    }
}

В использовании многопоточности очень много нюансов. Я не уверен, что все их грамотно преодолел в показанном коде.

Например, класс Random не является потокобезопасным. Поэтому нужно либо лочить доступ при обращении к его методам, либо использовать по экземпляру на поток (как сделал я). При этом нужно задать разные начальные значения (Seed) для них, чтобы они не стали выдавать одинаковые значения, т. к. создаются практически одновременно. Для этого я использовал хеш объектов.

Потокам я установливаю свойство IsBackground = true, чтобы они прекратили работу в любом случае при закрытии или вылете программы.

У каждого экземпляра класса Ball в потоках изменяются два свойства: X и Y. И может случиться так, что когда один поток задал значение X, другой поток в это время вызвал Invalidate и сработал обработчик события Paint, в котором будет прочитано новое значение X и старое значение Y. Для данной учебной программки это несущественно, но в реальном софте это может привести к багам.

Вызов Invalidate(); приведёт к перерисовке всего канваса. С одной стороны, это просто, с другой - не очень производительно. Поэтому я в коде показал закомментированные два вызова Invalidate(rectangle): первый вызов очищает канвас на предыдущем месте шарика, второй отрисует его на новом месте. Это, скорее всего, более эффективно. Но иногда может привести к тому, что часть области не очистится (у меня такое раз случилось при тестах) при изменении размера формы и т. п.

→ Ссылка