Постановка задачи. Требуется запрограммировать движение шара или шаров на бильярдном столе без учета трения, но с учетом упругого отражения от стенок бильярда. Границы стола — прямоугольник, без луз. (в новой редакции)
Начнем проектирование приложения с определения объектов и классов. Объектами являются бильярдный стол (пусть это будет форма) и шар(ы), для описания которых объявим класс 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: Наш Чат, в котором вы можете обсудить любые вопросы, идеи, поделиться опытом или связаться с администраторами.
![]() |
![]() |
![]() |
![]() |
В первой части мы же нигде не объявляем:
У меня методе Move() в строке g.Clear(Form1.BackC); ошибка:
Для нестатического поля, метода или свойства «анимация_бильярдный_шар.Form1.BackC» требуется ссылка на объект
Антон! При копировании из VS пропустил объявление полей класса Form1:
Спасибо, что заметили! На сайте исправил.
Ошибка у Вас из-за того, что поле BackC Вы не объявили как static.
Здравствуйте, у меня не появляется шарик, появляется только бильярдный стол(зеленый). Что не так?
Перечитайте (в 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
у меня код не запускается, то есть в коде указывается ошибки на ClientRectangle то что его не существует в этом контексте и при объявлении Sharik[] shars тоже ошибка
Айгерим !
Это второй пример с несколькими шарами? А с одним — работает?
Вы просто копировали текст в конце поста?
1) проверьте подключение библиотеки using System.Drawing;
2) Имя массива в тексте shar, а не shars (что было бы логичнее, но так назвал)
3) не получится — пришлите Ваш текст Form1.cs — я исправлю Ваши погрешности.
Можете связаться со мной по скайпу или черезTelegram голосом (на 1 странице сайта есть).
Дерзайте, все получится.
У меня такая вот ошибочка: Sharik: Имена членов не могут совпадать с именами типов, в которых они содержатся. Присылаю Вам текст Form1.cs
Технология Копировать/Вставить конечно эффективная, но иногда дает сбои. Вы два раза вставили определение класса, одно уберите — и все получится.
Весьма признателен коллеге Александру Э. за замечания и найденные ошибки, позволившие текст сделать более точным.