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

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

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

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

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

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

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

Напомним, что начало координат (0,0) находится в левом верхнем углу формы, ось х — горизонтальная, слева направо, ось y — вертикальная, направлена сверху вниз.
Цвет шара передадим через конструктор, используя его для задания поля — объект Кисть (Brush):
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 void Move(Graphics g),
где g — холст для рисования.
Смысл первого метода класса состоит в задании полей объекта «шар»:

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 = -dx;
   if (x < 0)
      dx = -dx;
   x += dx;
   if (y >= Form1.ActiveForm.Height- SystemInformation.CaptionHeight - 2 * (radius+dy))
      dy = -dy;
   if (y < 0)
      dy = -dy;
   y += dy;
}

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

Третий метод — public void Move(Graphics g) рисует каждое новое перемещение шара:

public void Move(Graphics g)
{
   g.Clear(Form1.BackC);
   Next(); // вычисление нового положения шара
   g.FillEllipse(br, x, y, 2 * radius, 2 * radius);
}

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

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

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

private void timer1_Tick(object sender, EventArgs e)
{
   shar.Move(g);
}

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

public partial class Form1 : Form
{
   public Sharik shar; // шар
   Graphics g; // холст (стол)
   static public Color BackC = Color.Green; // цвет стола
   
   public Form1()
   {
      InitializeComponent();
      this.BackColor = BackC;  
      this.Width = 800;   // длина стола
      this.Height = 400;  // ширина стола
      this.DoubleBuffered = true;
   }
   // загрузка
   private void Form1_Load(object sender, EventArgs e)
   {
      g = this.CreateGraphics();
   }

   // старт
   private void Form1_Click(object sender, EventArgs e)
   {
      shar = new Sharik(30, Color.Yellow, 5, 150, 10, 7);
      g.Clear(BackC);
      timer1.Enabled = true;      // запуск таймера
   }

   // при срабатывании таймера - перемещение
   private void Timer1_Tick(object sender, EventArgs e)
   {
      shar.Move(g);
   }

   // при изменении размеров стола
   private void Form1_SizeChanged(object sender, EventArgs e)
   {
      g = this.CreateGraphics();
   }
}   // конец описания класса

6. Полное описание класса Sharik

public class Sharik : Form1 // класс Шары
{
   int radius;        // радиус
   SolidBrush br;     // кисть для его рисования
   int x;             // координата x - слева
   int y;             // координата y - сверху
   int dx;            // шаг смещения по x
   int dy;            // шаг смещения по y

   // конструктор - передача параметров шара
   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;
   }
   // отражение от стенок 
   public void Next()
   {
      if (x >= Form1.ActiveForm.Width - 2 * (radius + dx))
         dx = -dx;
      if (x <= 0) dx = -dx; x += dx; if (y >= Form1.ActiveForm.Height - SystemInformation.CaptionHeight - 2 * (radius+dy))
         dy = -dy;
      if (y <= 0)
         dy = -dy;
      y += dy;
   }
   // перемещение
   public void Move(Graphics g)
   {
      g.Clear(Form1.BackC);
      Next();  // вычисление нового положения шара
      g.FillEllipse(br, x, y, 2 * radius, 2 * radius);
   }
}

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;                       // фактическое число шаров
readonly 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) При срабатывании таймера реализуется метод Tmer1_Tick(), выполняющий те же действия: стирание, сдвиг (функция Next()), рисование — что и для одного шара, но уже в цикле для N шаров.

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

/* Пример анимации - перемещение N бильярдных шаров
 (С) Рычков В.А. 2018/21 */
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 = 1700; // новая ширина стола
         this.Height = 800; // новая длина стола
      }
      // при загрузке формы
      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] = new Sharik(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(20, 50);
        x = r.Next(1, ClientRectangle.Width-2*radius);
        y = r.Next(1, ClientRectangle.Height-2*radius);
        dx = r.Next(10,20);
        dy = r.Next(8,15)-10;
     }
     // отражение от стенок бильярда
     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 шаров».

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


NEW: Наш Чат, в котором вы можете обсудить любые вопросы, идеи, поделиться опытом или связаться с администраторами.


Помощь проекту:

Понравилась статья? Поделиться с друзьями:
0 0 голоса
Рейтинг статьи
Подписаться
Уведомить о
9 комментариев
Новые
Старые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии

В первой части мы же нигде не объявляем:

public Color BackC = Color.Green;
Graphics g;
Sharik shar = new Sharik(2, Color.White, 20, 20, 1, 1);

У меня методе Move() в строке g.Clear(Form1.BackC); ошибка:
Для нестатического поля, метода или свойства «анимация_бильярдный_шар.Form1.BackC» требуется ссылка на объект

Важно: Вы можете поддержать проект и автора.

Здравствуйте, у меня не появляется шарик, появляется только бильярдный стол(зеленый). Что не так?

Важно: Вы можете поддержать проект и автора.

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

Важно: Вы можете поддержать проект и автора.

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

Важно: Вы можете поддержать проект и автора.
Важно: Вы можете поддержать проект и автора.
9
0
Оставьте комментарий! Напишите, что думаете по поводу статьи.x
()
x