Постановка задачи. Требуется запрограммировать движение шара или шаров на бильярдном столе без учета трения, но с учетом упругого отражения от стенок бильярда. Границы стола — прямоугольник, без луз.
Начнем проектирование приложения с определения объектов и классов. Объектами являются бильярдный стол (пусть это будет форма) и шар(ы), для описания которых объявим класс 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] = 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(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 шаров».
Рассмотрим пример анимации спрайтами.
Весьма признателен коллеге Александру Э. за замечания и найденные ошибки, позволившие текст сделать более точным.
у меня код не запускается, то есть в коде указывается ошибки на ClientRectangle то что его не существует в этом контексте и при объявлении Sharik[] shars тоже ошибка
Айгерим !
Это второй пример с несколькими шарами? А с одним — работает?
Вы просто копировали текст в конце поста?
1) проверьте подключение библиотеки using System.Drawing;
2) Имя массива в тексте shar, а не shars (что было бы логичнее, но так назвал)
3) не получится — пришлите Ваш текст Form1.cs — я исправлю Ваши погрешности.
Можете связаться со мной по скайпу или черезTelegram голосом (на 1 странице сайта есть).
Дерзайте, все получится.
У меня такая вот ошибочка: Sharik: Имена членов не могут совпадать с именами типов, в которых они содержатся. Присылаю Вам текст Form1.cs
Технология Копировать/Вставить конечно эффективная, но иногда дает сбои. Вы два раза вставили определение класса, одно уберите — и все получится.
Здравствуйте, у меня не появляется шарик, появляется только бильярдный стол(зеленый). Что не так?
Перечитайте (в 1 части — про один шарик)
7. Последние замечания. Общая последовательность действий:
1) Создаем проект — приложение Windows Form с именем проекта, например, анимация1. Растягиваем форму до желаемых размеров.
2) Помещаем на форму таймер (timer1 класса Timer), задаем в свойствах интервал 40 (миллисекунд). Событию Tick назначаем метод обработки события timer1_Tick( ).
3) Аналогичные действия выполняем для событий Form1.Load( ) и Form1.Click( ).
ОШИБКА: У Вас метод Form1_Click(object sender, EventArgs e) не связан с событием Form1.Click