Начала анимации. Движение шара на бильярдном столе

 Постановка задачи. Требуется запрограммировать движение шара или шаров на бильярдном столе без учета трения, но с учетом упругого отражения от стенок бильярда. Границы стола — прямоугольник, без луз.

Начнем проектирование приложения с определения объектов и классов. Объектами являются бильярдный стол (пусть это будет форма) и шар(ы), для описания которых объявим класс Sharik, эти объекты взаимодействуют между собой.

Рассмотрим сначала перемещение одного шара, а затем изменим программу для N шаров.

Обсуждение будет более детальное и состоит из 7 пунктов. Продолжаем мыслить в объектах:

1. Бильярдный стол — все окно формы — Form1 (кроме заголовка). Имеет цвет — BackColor, размеры: Width – ширина,  Height – высота. Графический объект — холст (для анимации) Graphics g;

2. Шар — объект со своими полями и методами. Так как их может быть более одного, создадим сразу класс (вне стандартного класса Form1) Sharik.
Шар характеризуется радиусом, цветом, местоположением на плоскости:
public int radius;       // радиус
public int x;            // координата x — левая граница шара
public int y;            // координата y — граница шара сверху

Напомним, что начало координат (0,0) находится в левом верхнем углу формы, ось х — горизонтальная, слева направо, ось y — вертикальная, направлена сверху вниз.
Поскольку предполагаем использовать радиус и координаты через прямое обращение к полям объекта, то нам следует сделать их общедоступными (public). Цвет шара передадим через конструктор, используя его для задания поля — объект Кисть (Brush):
public SolidBrush br;           // кисть для рисования шара
Перемещение по прямой предполагает изменение каждой из координат на постоянные приращения — шаги смещения (могут быть >0, =0, <0):
int dx;         // шаг смещения по x
int dy;        // шаг смещения по y
При таком объявлении полей они являются закрытыми  членами (private) класса по умолчанию (термин default по англ.).

3. Определим два метода класса Sharik.
Первый — конструктор, для задания характеристик шара:
public Sharik(int r, Color c, int x0, int y0, int d_x, int d_y).
Второй — для определения смещения шара на каждом такте:
public void Next().
Смысл первого метода класса состоит в задании полей объекта «шар»:

public Sharik(int r, Color c, int x0, int y0, int d_x, int d_y)
{
   radius = r;
   br = new SolidBrush(c);
   x = x0;
   y = y0;
   dx = d_x;
   dy = d_y;
}

Конструктор  Sharik( ) обеспечивает начальное задание 6 полей объекта «шар» передачей 6 па­раметров. Поле br является объектом класса SolidBrush (обеспечивает сплошную за­ливку фигуры заданным цветом). Объявление объектов класса Brush невозможно (если хоти­те — попробуйте), так как он является абстрактным, но возможно объявление объектов на­следуемых классов SolidBrush, TextureBrush и LinearGradientBrush.

Второй  метод public void Next(), используемый для определения  координат шара на каждом следующем такте рисования, казалось бы предельно прост:
x += dx;
y += dy;
что, как вы понимаете, соответствует x = x + dx;  y = y + dy;
Однако, это будет правильно до тех пор, пока шар не достигнет какой-либо границы (стенки бильярдного стола).
И вот тут нам понадобится информация об объекте «форма» (Form1), который определен в другом классе :
public partial class Form1 : Form { }.
Нам будут нужно всего лишь свойства формы Width и Height. Проще всего это сделать через активную форму приложения:

Form1.activeForm.Width  и Form1.Active.Heigth.

Проверяя условия достижения границ (с учетом размера шара), легко догадаться, что отражение от стенки будет означать просто смену знака приращения dx или dy. Тогда метод Next() может быть в первом приближении определен так:

// перемещение и/или отражение от стенок бильярда
public void Next()
{
   if (x >= Form1.ActiveForm.Width - 2*radius)
      dx = -dx;
   if (x < 0)
      dx = -dx;
   x += dx;
   if (y >= Form1.ActiveForm.Height-2*radius)
      dy = -dy;
   if (y < 0)
      dy = -dy;
   y += dy;
}

Примечание. Если вы захотите изменять траекторию другим способом, например, по некото­рой кривой, то вам придется переопределить только этот метод Next( ). Возможно, что к свойствам шара придется добавить какие-либо параметры (через конструктор Sharik( )). Тогда применяйте другие два принципа ООП: наследование и полиморфизм.

4. Реализация движения шара

 Вспомним принцип кино «24 кадра» (в секунду). Если мы с некоторой частотой будем стирать предыдущее изображение шара, и затем будем рисовать его не далеко от предыдущего места, то мы сможем получить иллюзию движения.

Для задания тактов удобно использовать невизуальный компонент timer, который находится на Панели элементов. В окне свойств для этого объекта задайте свойство Interval=40. Интервал задается в миллисекундах, это означает, что за 1 секунду произойдет 25 срабатываний таймера (вот вам и частота кадров — 25 в секунду). Тогда метод, реагирующий на событие  timer1.Tick, может быть определен так:

private void timer1_Tick(object sender, EventArgs e)
{
   g.FillEllipse(brf, shar.x, shar.y, shar.radius, shar.radius);
   shar.Next();  // вычисление нового положения шара
   g.FillEllipse(shar.br, shar.x, shar.y, shar.radius, shar.radius);
}

Интересно, что для отображения всей траектории движения достаточно удалить (про­ще всего — закомментировать) оператор стирания:
g.FillEllipse(brf, shar.x, shar.y, shar.radius, shar.radius);

5. Полное описание класса Form1 приведено ниже.

public partial class Form1 : Form
{
    public Sharik shar;   // объявление объекта — шар
    Graphics g;      // холст 
    SolidBrush brf;  // кисть для фона 
    public Form1()   // вызов конструктора формы
    {
       InitializeComponent(); // вставляется автоматически
       this.BackColor = Color.Green; // изменяем цвет стола на зеленый
       brf = new SolidBrush(BackColor);
       this.Width = 1000;              // новая ширина формы
       this.Height = 500;              // новая высота формы
    }
 //  На закладке События Form1 находим Load и  
 //  создаем заготовку метода Form1_Load( ), добавив оператор
    private void Form1_Load(object sender, EventArgs e)
    {
       g = this.CreateGraphics(); // создаем объект - холст 
    }
 // Старт рисования свяжем с событием  Form1.Click, создадим 1 шарик
 // сделаем таймер доступным
    private void Form1_Click(object sender, EventArgs  e)
    {
       shar = new Sharik(50, Color.Yellow, 5, 150, 6, -5);   
       timer1.Enabled = true;  // запуск таймера
    }
 // Четвертый, последний метод класса, при срабатывании таймера
    private void timer1_Tick(object sender, EventArgs e)
    {
       g.FillEllipse(brf, shar.x, shar.y, 2*shar.radius, 2*shar.radius);
       shar.Next();          // вычисление нового положения шара
       g.FillEllipse(shar.br, shar.x, shar.y, 2*shar.radius, 2*shar.radius);  
    }
}   // конец описания класса

6. Описание класса Sharik

public class Sharik : Form1 // класс Шары
{
   public int radius; // радиус
   public SolidBrush br; // кисть для его рисования
   public int x; // координата x - слева
   public int y; // координата y - сверху
   int dx; // шаг смещения по x
   int dy; // шаг смещения по y
   // передача параметров шара
   public void Sharik(int r, Color c, int x0, int y0, int d_x, int d_y)
   {
         // см. пункт 3
   }
   // отражение от стенок бильярда
   public void Next()
   {
        // см. пункт 3
   }
}

7. Последние замечания. Общая последовательность действий:
1) Создаем проект — приложение Windows Form с именем проекта, например, анимация1. Растягиваем форму до желаемых размеров.
2) Помещаем на форму таймер (timer1 класса Timer),  задаем в свойствах интервал 40 (миллисекунд). Событию Tick назначаем метод обработки события timer1_Tick( ).
3) Аналогичные действия выполняем для событий Form1.Load( ) и Form1.Click( ).
4) Вставляем описания классов Form1 и Sharik, как указано выше. Не ленитесь записывать комментарии.
5) Запустите готовое приложение.
Внесите в текст изменения, анализируйте результаты и возможные ошибки.

Почти конец примера?

Зададимся вопросом, а насколько усложнится данная программа, если по бильярдному столу будет перемещаться несколько шаров: от 1 до 50 ?

 Оказывается, что это сделать совсем просто. Просто нужно задать N объектов-шаров и посмотреть что получится. Это дополнение к примеру демонстрирует преимущества ООП.  Нам понадобится изменить (текст файла Form1.cs  приведен ниже):

1) Объекты, объявляемые в классе Form1:
public Graphics g;                    // холст
const int N_max = 50;             // максимальное число шаров
public Sharik [] shar = new Sharik[N_max];  // объявление массива шаров
public int N = 10;                       // фактическое число шаров
Random r = new Random(); // случайное число
SolidBrush brf; // кисть фона

Пусть максимальное число шаров — 50. Константа N_max нужна для объявления размерности массива объектов-шаров (резервируется место только для ссылок. Помним, что массив в C# является ссылочным типом). Переменной N (фактическое число шаров) присвоим пока какое-нибудь значение, например, 10. Далее придумаем, как число шаров можно менять.
Для задания массива шаров (ссылок) используем конструктор массива (третья строка) вместо  объявления public Sharik shar;  для одного шарика в исходном примере.

Для демонстрации анимации удобно задавать свойства шаров случайным образом, для этого пригодится переменная r класса Random.

2) Заменим в классе Sharik конструктор
Sharik(int r, Color c,  int x0, int y0, int d_x, int d_y)
на конструктор
Sharik(int rch)
c одним параметром — случайным числом.
Поскольку мы будем его вызывать N раз (по числу создаваемых шаров), то этот параметр обеспечит нам гарантированное разнообразие (см. описание класса Random  в  справке Microsoft). По сути, мы ввели конструктор с тем же именем, но с другим (меньшим!) числом параметров. Это  действие определяется в C# термином «перегрузка».

3) В класс Sharik добавим метод RandomColor(int_rch) для изменения цвета шаров. Также (через метод Random.Next( ))  будем в конструкторе изменять все размеры.

4) Для задания числа шаров в верхнем левом углу формы разместим компоненты label1 (“Выберите количество шаров:») и comboBox1 (со списком возможного числа шаров, например: 1,2,3,5,7,10,15,20,50 — свойство Items «Коллекция», можете ее менять).

5)  Для обработки события  SelectedValueChanged (выбор позиции списка) у comboBox1 используем метод

private void comboBox1_SelectedValueChanged( ),

с помощью которого изменяем число шаров, делаем невидимыми объекты comboBox1 и label1 и запускаем метод  Form1_Click().

6)  Метод Form1_Click() задает и инициализирует объекты-шары, задает их свойства, запускает таймер и очищает бильярдный стол от чего бы то на нем не было.

7) При срабатывании таймера реализуется метод timer1_Tick(), выполняющий те же действия: стирание, сдвиг (функция Next()), рисование — что и для одного шара, но уже в цикле для N шаров.

Вот полный текст файла Form1.cs (полезно сравнить его с файлом для одного шара, чтобы понять суть комментариев, общность и отличия):

/* Пример анимации - перемещение N бильярдных шаров
 (С) Рычков В.А. 2018 */
using System;
using System.Drawing;
using System.Windows.Forms;
namespace анимация1
{
   public partial class Form1 : Form
   {
      public Graphics g;     // холст
      const int N_max = 50;  // максимальное число шаров
      public Sharik [] shar = new Sharik[N_max]; 
          // объявление объектов - массив шаров
      public int N = 10;   // фактическое число шаров
      Random r = new Random();    // случайное число
      SolidBrush brf;

      // конструктор формы
      public Form1()
      {
         InitializeComponent();
         this.BackColor = Color.Green; // цвет сукна
         this.Width = 1350; // новая ширина стола
         this.Height = 700; // новая длина стола
      }
      // при загрузке формы
      private void Form1_Load(object sender, EventArgs e)
      {
         g = CreateGraphics();  // объект - на форме
      }
      // реакция на клик по форме
      private void Form1_Click(object sender, EventArgs e)
      {
         N = Convert.ToInt32(comboBox1.Text);
         int rch;
         for (int i = 0; i < N; i++)
         {
            rch=r.Next(100000);
            shar[i].Sharik_new(rch);
         }  
         timer1.Enabled = true;  // запуск таймера
         g.Clear(BackColor);   // очистка поля от шаров
         brf = new SolidBrush(BackColor);
      }
   
      // при срабатывании таймера
      private void timer1_Tick(object sender, EventArgs e)
      {
          Sharik s;  // ссылка на объект класса Sharik
          for (int i=0; i<N; i++)
          {
             s=shar[i];
             // очистка холста от ранее нарисованного шара
             g.FillEllipse(brf, s.x, s.y, 2 * s.radius, 2 * s.radius);  
             s.Next();  // его новое расположение через сдвиг
             // изображение i-го шара после сдвига
             g.FillEllipse(s.br, s.x + 2, s.y + 2, 2 * s.radius - 4, 2 * s.radius - 4);
          }
      }
      // изменение числа шаров
      private void comboBox1_SelectedValueChanged(object sender, EventArgs e)
      {
         N = Convert.ToInt32(comboBox1.Text);
         comboBox1.Visible = false;
         label1.Visible = false;
         Form1_Click(sender,e);
      }
   }
  // класс Шары
  public class Sharik 
  {
     public int radius;      // радиус
     public SolidBrush br;   // кисть для его рисования
     public int x;           // координата x - слева
     public int y;           // координата y - сверху
     int dx;                 // шаг смещения по x
     int dy;                 // шаг смещения по y
     // задание свойств шара
     public void Sharik(int rch)
     {
        Random r = new Random(rch); // для новой цепочки 
             //   случайных чисел через rch
        br = new SolidBrush(RandomColor(rch));
        radius = r.Next(30, 50);
        x = r.Next(1, ClientRectangle.Width-2*radius);
        y = r.Next(1, ClientRectangle.Height-2*radius);
        dx = r.Next(2,6);
        dy = r.Next(3,5);
     }
     // отражение от стенок бильярда
     public void Next()
     {
        if (x >= Form1.ActiveForm.Width-2*radius)
           dx = -dx;
        if (x <= 0)
           dx = -dx;
        x += dx;
        if (y >= Form1.ActiveForm.Height-2*radius)
           dy = -dy;
        if (y <= 0)
           dy = -dy;
        y += dy;
     }
     // случайный цвет
     public Color RandomColor(int rch)      // rch - случайное число
     {
        int r, g, b;
        byte[] bytes1 = new byte[3];        // массив 3 цветов
        Random rnd1 = new Random(rch);
        rnd1.NextBytes(bytes1);             // генерация в массив 
        r = Convert.ToInt16(bytes1[0]);
        g = Convert.ToInt16(bytes1[1]);
        b = Convert.ToInt16(bytes1[2]);
        return Color.FromArgb(r, g, b);     // возврат цвета 
     }
  }    
}

Запустите программу. Сначала выбираем количество шаров. Каждый клик на форме генерирует новый набор шаров (по цвету, размерам и динамике).

Сравните ваш результат с видеороликом: 10 шаров

Подумайте, какие недостатки есть у такой анимации, что бы вы можете изменить?

  Конец  примера «N шаров».

Рассмотрим пример анимации спрайтами.

5 мысли о “Начала анимации. Движение шара на бильярдном столе”

  1. Вячеслав Рычков

    Весьма признателен коллеге Александру Э. за замечания и найденные ошибки, позволившие текст сделать более точным.

  2. Айгерим

    у меня код не запускается, то есть в коде указывается ошибки на ClientRectangle то что его не существует в этом контексте и при объявлении Sharik[] shars тоже ошибка

    1. Вячеслав Рычков

      Айгерим !
      Это второй пример с несколькими шарами? А с одним — работает?
      Вы просто копировали текст в конце поста?
      1) проверьте подключение библиотеки using System.Drawing;
      2) Имя массива в тексте shar, а не shars (что было бы логичнее, но так назвал)
      3) не получится — пришлите Ваш текст Form1.cs — я исправлю Ваши погрешности.
      Можете связаться со мной по скайпу или черезTelegram голосом (на 1 странице сайта есть).
      Дерзайте, все получится.

      1. У меня такая вот ошибочка: Sharik: Имена членов не могут совпадать с именами типов, в которых они содержатся. Присылаю Вам текст Form1.cs

        1. Вячеслав Рычков

          Технология Копировать/Вставить конечно эффективная, но иногда дает сбои. Вы два раза вставили определение класса, одно уберите — и все получится.

Оставьте комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *