Класс Particle: ООП в p5
Сегодня одно описание «как устроено перо» превратится в фабрику, из которой можно достать хоть сотню летающих перьев.
Класс — это шаблон объекта: чертёж, по которому p5 штампует сколько угодно одинаковых по устройству, но разных по состоянию частиц. Каждая частица знает, где она (поля) и что умеет (методы).
В прошлый раз мы научились хранить позицию и скорость частицы в одном удобном объекте — p5.Vector. Сегодня сделаем следующий шаг: завернём всю частицу-перо целиком в собственный шаблон, чтобы создавать её копии одной строчкой. Это входной билет в большие системы частиц — дым, искры, фейерверк над головой CodeChick.
Зачем заворачивать перо в класс
Представь, что ты собираешь стикерпак для чата. Ты же не рисуешь заново каждый стикер с нуля — ты делаешь один шаблон стикера, а потом штампуешь его в разных позах и цветах. Класс в программировании — ровно такой шаблон. Ты один раз описываешь, как устроено перо CodeChick, а дальше создаёшь хоть одно перо, хоть триста — каждое со своими координатами и скоростью.
Вот к чему мы придём за этот урок:
let feathers = [];
function setup() {
createCanvas(600, 600);
for (let i = 0; i < 60; i++) {
feathers.push(new Particle(width / 2, height / 2)); // 60 перьев из одного гнезда
}
}
function draw() {
background(20, 24, 40);
for (let f of feathers) {
f.update(); // каждое перо двигается само
f.display(); // и само себя рисует
}
}
class Particle {
constructor(x, y) {
this.pos = createVector(x, y);
this.vel = createVector(random(-2, 2), random(-2, 2));
}
update() {
this.pos.add(this.vel);
}
display() {
noStroke();
fill(255, 220, 90);
ellipse(this.pos.x, this.pos.y, 8, 8);
}
}Результат: на тёмно-синем небе из центра холста разлетается облако из 60 жёлтых пёрышек CodeChick. Каждое летит в свою сторону со своей скоростью, как будто цыплёнок встряхнулся и обронил перья во все стороны разом. И весь этот рой описан всего одним маленьким классом Particle.
Если бы мы хранили координаты каждого пера отдельными переменными — x1, y1, x2, y2 и так до шестидесяти — код превратился бы в простыню на пол-экрана, и добавить 61-е перо было бы пыткой. Класс решает это раз и навсегда: одно описание, сколько угодно копий.
И ещё одна приятная мелочь: когда вся частица собрана в одном месте, её удобно дорабатывать. Захотел, чтобы перо вращалось в полёте? Добавляешь одно поле и пару строк в методы — и сразу все шестьдесят перьев начинают крутиться. Не нужно искать и править десятки разрозненных переменных по всему скетчу. Класс держит всё, что касается пера, в одной коробке — это называют инкапсуляцией, и именно она спасает большие проекты от превращения в кашу.
Что такое класс (на пальцах)
Вернёмся к метафоре чертежа. Представь форму для печенья — ту самую звёздочку или сердечко, которой выдавливают фигурки из теста. Сама форма — это не печенье. Это шаблон. А вот каждое печенье, которое ты ею вырезал, — настоящий объект: у одного больше изюма, другое подгорело с краю, третье кривое. Все они сделаны по одной форме, но живут своей жизнью.
Класс — это форма для печенья. Объект — это конкретное печенье, вырезанное этой формой.
В нашем случае класс Particle — это форма «перо CodeChick». А когда мы пишем new Particle(300, 300), p5 берёт эту форму и выдавливает из неё одно настоящее перо в точке (300, 300). Слово new и значит «создай по шаблону новый объект».
Поля: что частица знает о себе
У каждого объекта есть его собственные данные — то, что отличает одну частицу от другой. Эти данные называются полями (или свойствами). У нашего пера два поля:
this.pos— где перо находится прямо сейчас (вектор позиции);this.vel— куда и как быстро оно летит (вектор скорости).
Слово this здесь — самое важное. this означает «вот это конкретное перо, о котором сейчас речь». Когда у тебя сотня перьев, каждое говорит про себя this.pos — и каждое имеет в виду свои координаты, а не соседские. Думай про this как про слово «моё»: моя позиция, моя скорость.
Методы: что частица умеет
Кроме данных, у объекта есть действия — то, что он умеет делать. Эти действия называются методами. По сути метод — это обычная функция, только живущая внутри класса и имеющая доступ к полям через this. У нашего пера два метода:
update()— пересчитать позицию: прибавить скорость к координатам (перо делает шаг);display()— нарисовать себя на холсте в текущей позиции.
Запомни это разделение, оно фундаментальное: update() думает, display() рисует. Один метод меняет числа, другой превращает числа в картинку. Дальше ты увидишь это разделение в любой системе частиц.
Конструктор: момент рождения частицы
Осталась одна особая функция — constructor. Это метод, который p5 вызывает один раз в момент, когда частица рождается через new. Его задача — заполнить поля стартовыми значениями. В нашем пере конструктор получает координаты x, y, ставит туда позицию и выдаёт перу случайную скорость, чтобы все перья разлетелись по-разному.
Метафора: конструктор — это как анкета новорождённого. Перо появилось на свет — и тут же заполняет графы «где я» и «куда лечу». После этого оно начинает жить: каждый кадр обновляется и рисуется.
Разбираем класс по кусочкам
Пример 1. Одно перо, которое живёт само
Начнём с самого скромного — одно-единственное перо. Никаких массивов, никаких циклов: просто покажем, как объект хранит своё состояние между кадрами и сам себя двигает.
let feather;
function setup() {
createCanvas(600, 600);
feather = new Particle(300, 300); // родили одно перо в центре
}
function draw() {
background(20, 24, 40);
feather.update(); // перо делает шаг
feather.display(); // и рисует себя
}
class Particle {
constructor(x, y) {
this.pos = createVector(x, y);
this.vel = createVector(2, -1); // вправо и чуть вверх
}
update() {
this.pos.add(this.vel);
}
display() {
noStroke();
fill(255, 220, 90);
ellipse(this.pos.x, this.pos.y, 12, 12);
}
}Результат: на тёмном небе появляется одно жёлтое пёрышко в центре и плавно уплывает вправо-вверх, оставляя холст за собой. Каждый кадр оно сдвигается на 2 пикселя вправо и на 1 вверх — потому что такую скорость мы записали в его поле vel.
Разберём, что здесь происходит по шагам:
feather = new Particle(300, 300)вsetup()— рождаем перо. Срабатывает конструктор:this.posстановится вектором (300, 300), аthis.vel— вектором (2, -1).- Каждый кадр
draw()зовётfeather.update()— внутри методаthis.pos.add(this.vel)прибавляет скорость к позиции. Координаты пера меняются. - Затем
feather.display()рисует жёлтый кружок в новойthis.pos.
Ключевая идея: поля pos и vel живут между кадрами. В обычной переменной внутри draw() значение стиралось бы каждый кадр, а поля объекта помнят своё состояние — поэтому перо плавно летит, а не дёргается на месте.
Пример 2. Конструктор с параметрами — перья из разных гнёзд
Сила класса в том, что одну и ту же форму можно выдавливать в разных местах. Раз конструктор принимает x, y, мы можем родить три пера в трёх разных точках — и каждое заживёт своей жизнью.
let a, b, c;
function setup() {
createCanvas(600, 600);
a = new Particle(150, 100);
b = new Particle(300, 100);
c = new Particle(450, 100);
}
function draw() {
background(20, 24, 40);
for (let p of [a, b, c]) {
p.update();
p.display();
}
}
class Particle {
constructor(x, y) {
this.pos = createVector(x, y);
this.vel = createVector(random(-1, 1), 2); // у каждого свой случайный снос вбок
}
update() {
this.pos.add(this.vel);
}
display() {
noStroke();
fill(255, 220, 90);
ellipse(this.pos.x, this.pos.y, 10, 10);
}
}Результат: от верхнего края падают три жёлтых пёрышка из трёх разных точек. Все опускаются вниз, но каждое чуть сносит вбок по-своему — у одного влево, у другого вправо, у третьего почти прямо. Они стартовали из одной формы, но random в конструкторе сделал каждое уникальным.
Главное здесь — увидеть, что a, b и c — это три независимых объекта. У каждого свой this.pos и свой this.vel. Когда мы зовём a.update(), меняется только позиция a; b и c этого даже не замечают. Слово this каждый раз указывает на тот объект, у которого вызвали метод.
Пример 3. Массив частиц — настоящая стая перьев
Три переменные с именами — это всё ещё ручная работа. А что, если перьев шестьдесят? Тут на помощь приходит массив: храним все частицы в одном списке и обходим его циклом. Это и есть зачаток системы частиц.
let feathers = [];
function setup() {
createCanvas(600, 600);
for (let i = 0; i < 60; i++) {
feathers.push(new Particle(width / 2, height / 2));
}
}
function draw() {
background(20, 24, 40, 40); // полупрозрачный фон оставляет шлейф
for (let f of feathers) {
f.update();
f.display();
}
}
class Particle {
constructor(x, y) {
this.pos = createVector(x, y);
this.vel = createVector(random(-3, 3), random(-3, 3));
}
update() {
this.pos.add(this.vel);
}
display() {
noStroke();
fill(255, 220, 90);
ellipse(this.pos.x, this.pos.y, 6, 6);
}
}Результат: из центра холста взрывом разлетается облако из 60 жёлтых перьев во все стороны, как от встряхнувшегося цыплёнка. Благодаря полупрозрачному фону (четвёртый параметр 40 в background) за каждым пером тянется лёгкий хвост-шлейф, и весь рой выглядит как праздничный салют над CodeChick.
Разберём связку «массив + цикл»:
let feathers = []— заводим пустой список будущих перьев.- Цикл
forвsetup()шестьдесят раз создаётnew Particle(...)и кладёт его в массив черезpush(). - В
draw()циклfor...ofпроходит по всем перьям и каждому говоритupdate()иdisplay().
Обрати внимание, насколько спокойно класс пережил рост от одного пера до шестидесяти: код класса не изменился вообще. Поменялось только то, сколько копий мы из него достали. Именно за это программисты так любят классы — описал один раз, используй сколько нужно.
Частые ошибки и подводные камни
Классы поначалу спотыкают почти всех в одних и тех же местах. Давай пройдёмся по граблям заранее, чтобы ты на них не наступил.
1. Забыл this перед полем
Самая частая ошибка. Внутри метода ты пишешь pos.add(vel) вместо this.pos.add(this.vel). Без this программа решит, что pos — это какая-то локальная переменная, не найдёт её и упадёт с ошибкой вроде pos is not defined. Любое обращение к полю объекта изнутри метода — только через this.
2. Забыл new при создании
Если написать let f = Particle(300, 300) без new, объект не родится. Конструктор без new в современном JavaScript просто бросит ошибку, а f останется пустым. Запомни: рождение объекта по классу — всегда через new ИмяКласса(...).
3. Все частицы летят одинаково
Создаёшь шестьдесят перьев, а они слиплись в одну точку и движутся как одно целое. Скорее всего, ты задал скорость фиксированным числом — например, createVector(2, 2) — для всех. Тогда у всех частиц одинаковый vel, и они летят синхронно, перекрывая друг друга. Чтобы рой ожил, разбрасывай стартовую скорость через random прямо в конструкторе.
4. Перепутал update() и display()
Новички иногда суют в display() строку this.pos.add(this.vel) или, наоборот, рисуют кружок внутри update(). Технически сработает, но это путь к хаосу: как только частица станет сложнее, ты не разберёшься, где у тебя логика, а где рисование. Держи границу железно: update() меняет числа, display() их рисует.
5. Создаёшь частицы в draw() вместо setup()
Если поставить цикл feathers.push(new Particle(...)) внутри draw(), новые перья будут рождаться каждый кадр — шестьдесят раз в секунду. Массив за пару секунд раздуется до тысяч объектов, скетч начнёт тормозить и подвиснет. Создавай стартовый набор частиц один раз в setup(). (Позже мы научимся осознанно добавлять частицы в draw() — но только по чуть-чуть и с удалением старых.)
Мини-практика: оживи перо
Теперь твоя очередь. Возьми код из примера 3 и доработай частицу — добавь перу пару новых полей и научи его новым трюкам. Класс легко расширять: добавил поле в конструктор, использовал его в методах.
- Дай каждому перу свой размер. Заведи поле
this.size = random(4, 12)в конструкторе и используй его вdisplay()вместо жёсткой шестёрки. Рой станет разнокалиберным и живым. - Раскрась перья. Добавь поле
this.colсо случайным оттенком, напримерcolor(random(200, 255), random(180, 230), random(60, 120)), и применяй его вfill(). Получится тёплая палитра жёлто-оранжевых перьев. - Добавь гравитацию. В
update()перед сложением допишиthis.vel.y += 0.1— и перья начнут плавно опадать вниз, как настоящие. - Дай перу прозрачность, которая тает. Заведи поле
this.alpha = 255, вupdate()уменьшай его (this.alpha -= 2), а вdisplay()используй вfill(255, 220, 90, this.alpha). Перья будут медленно растворяться в небе.
Цель-максимум: собери облако перьев, в котором каждое — своего размера, своего оттенка, плавно падает и постепенно тает. Это уже почти настоящая система частиц, только пока без рождения новых. Поменяй пару чисел, посмотри на результат, поменяй ещё — так и рождается красивый эффект.
Итоги
Сегодня ты сделал важный шаг от отдельных переменных к настоящему ООП-мышлению.
- Класс — это шаблон (форма для печенья), по которому создаются объекты.
- Объект рождается через
new ИмяКласса(...)— каждый со своим состоянием. - Поля (
this.pos,this.vel) хранят данные частицы и живут между кадрами. - Методы — действия частицы; держи правило
update()думает,display()рисует. - constructor заполняет поля в момент рождения;
thisвсегда указывает на «вот этот конкретный объект». - Массив объектов + цикл = зачаток системы частиц: один класс, сколько угодно копий.
У тебя в руках теперь есть кирпичик, из которого строятся все большие эффекты этого раздела. Дальше мы превратим этот рой перьев в полноценную систему: научим частицы рождаться по ходу дела, стареть и умирать, чтобы из них можно было собрать дым, искры и взрыв фейерверка над головой CodeChick. Увидимся в следующем уроке!