Рисуем спрайты и фигуры
Сегодня ты впервые увидишь своего цыплёнка на экране: сначала из простых фигур, а потом — настоящей картинкой-спрайтом.
Спрайт — это картинка игрового объекта (героя, врага, монетки), которую мы рисуем на canvas. Любой герой любой игры начинается именно с этого.
Зачем вообще учиться рисовать фигуры
Представь, что ты открыл свою любимую игру — неважно, мобильную аркаду или что-то на компьютере. Всё, что ты там видишь: герой, монетки, кнопки, полоска здоровья, фон — это нарисованные пиксели. Игра тысячи раз в секунду перерисовывает картинку, и кажется, что персонаж живой. Но под капотом — простые команды вроде «нарисуй прямоугольник вот тут» и «выведи картинку героя вот сюда».
В прошлом уроке про canvas и контекст 2D ты уже подготовил холст и достал из него объект context — тот самый пульт, через который мы отдаём команды рисования. Сегодня мы наконец нажмём на этом пульте первые кнопки. К концу урока у тебя на экране появится площадка: зелёная земля, голубое небо, монетка-кружок и — главное — настоящий цыплёнок, твой герой на все следующие уроки курса.
Звучит как много? На самом деле это всего штук пять команд. Давай разберём их по одной, по принципу «меняем строчку — смотрим, что вышло».
И ещё одна хорошая новость: эти же пять команд работают в любой игре, которую ты потом захочешь сделать. Понг, Змейка, платформер — всё рисуется прямоугольниками, кругами и спрайтами. Ты не учишь что-то одноразовое «для урока»: прямо сейчас ты собираешь набор инструментов, которым будешь пользоваться до самого конца курса. Поэтому не торопись — лучше один раз понять, как ведёт себя каждая команда, чем потом гадать, почему монетка съехала на полэкрана или почему цыплёнок упорно не появляется.
Холст — это лист в клеточку
Самая удобная метафора для canvas — это лист бумаги в клеточку, только клеточки тут размером в один пиксель. Левый верхний угол — это точка с координатами (0, 0). Ось x растёт вправо, а ось y — вниз, а не вверх, как на уроках математики. Это первое, обо что спотыкаются все новички: чем больше y, тем ниже объект на экране.
Запомни так: верх экрана — это y = 0, и чем ниже ты опускаешься, тем число больше. Цыплёнок, который «падает вниз», на самом деле увеличивает свой y. Это пригодится уже в следующих уроках, когда мы добавим гравитацию.
Все команды рисования мы отдаём через контекст. Договоримся, что он у нас уже получен из прошлого урока вот такой строчкой:
const canvas = document.getElementById('game');
const context = canvas.getContext('2d');Результат: на странице есть пустой холст-canvas, и в переменной context лежит «пульт», через который дальше идут все команды рисования. Пока на экране ничего нет — белый прямоугольник.
Пример 1: рисуем прямоугольник — это будет земля
Самая частая фигура в играх — прямоугольник. Платформа, стена, кнопка, полоска здоровья, фон — всё это прямоугольники. Команда для заливки одна: context.fillRect(x, y, ширина, высота). Первые два числа — координаты левого верхнего угла фигуры, вторые два — её размеры.
Но перед тем как рисовать, надо выбрать цвет. За цвет заливки отвечает свойство context.fillStyle. Важная деталь: цвет — это «кисточка». Ты один раз макаешь кисточку в краску, и дальше всё, что рисуешь, идёт этим цветом — пока не поменяешь. Нарисуем небо и землю:
// небо — большой голубой прямоугольник на весь холст
context.fillStyle = 'skyblue';
context.fillRect(0, 0, 480, 360);
// земля — зелёная полоса внизу
context.fillStyle = 'forestgreen';
context.fillRect(0, 300, 480, 60);Результат: весь холст залит голубым небом, а внизу появляется зелёная полоса земли высотой 60 пикселей. Получилась простая сцена-задник, как фон в платформере.
Разберём по шагам, что произошло:
- Сначала
fillStyle = 'skyblue'— макнули кисточку в голубой. fillRect(0, 0, 480, 360)— нарисовали прямоугольник от левого верхнего угла на всю ширину и высоту. Это закрасило фон.- Потом сменили кисточку на
'forestgreen'и нарисовали полоску, начиная сy = 300(то есть в нижней части), высотой 60.
Обрати внимание на порядок: сначала рисуется небо, потом земля поверх него. На canvas всё работает как со слоями краски — то, что нарисовано позже, ложится сверху. Поэтому фон рисуют первым, а героя — последним.
Цвета можно задавать тремя способами
Цвет в fillStyle можно указывать как тебе удобнее:
- Словом —
'red','skyblue','gold'(браузер знает кучу названий). - Шестнадцатеричным кодом —
'#ffcc00'(как в графических редакторах и CSS). - Через
rgb—'rgb(255, 204, 0)', где три числа от 0 до 255 — это доля красного, зелёного и синего.
Для игр удобнее всего hex-коды: их легко скопировать из палитры в фотошопе или из любого инструмента подбора цветов, и оттенок будет ровно тот, что ты задумал. Но на старте слова читаются понятнее, поэтому в примерах я часто буду писать словами — так код проще читать вслух и держать в голове. Когда дойдёшь до настоящей игры, наверняка перейдёшь на hex: с ним легко собрать целую палитру уровня и быстро её менять.
Пример 2: рисуем круг — это будет монетка
С кругами чуть хитрее: у canvas нет команды «нарисуй круг». Вместо этого есть универсальный инструмент context.arc(), который рисует дугу, а круг — это просто полная дуга на 360 градусов. Углы тут задаются не в градусах, а в радианах, и полный круг — это Math.PI * 2. Запомни эту формулу как заклинание: 0, Math.PI * 2 — значит «полный круг».
Рисование круга — это всегда три шага: начать путь, описать дугу, залить. Сделаем золотую монетку:
context.fillStyle = 'gold';
context.beginPath();
context.arc(240, 180, 20, 0, Math.PI * 2);
context.fill();Результат: в центре холста появляется золотой кружок-монетка радиусом 20 пикселей. На фоне неба и земли из прошлого примера это выглядит как монетка, висящая в воздухе.
Разберём команды по порядку:
beginPath()— говорим «начинаю новый рисунок-контур». Это как поднять ручку и приставить её к бумаге в новом месте.arc(240, 180, 20, 0, Math.PI * 2)— описываем дугу. Числа: центр поx(240), центр поy(180), радиус (20), начальный угол (0) и конечный угол (полный круг).fill()— заливаем получившийся контур текущим цветом (золотым).
Заметь разницу с прямоугольником: у fillRect координаты — это угол фигуры, а у arc — это центр круга. Из-за этого новички часто рисуют круг не там, где ожидали. Держи это в голове.
Обводка вместо заливки
Иногда нужна не заливка, а контур — например, обвести монетку рамкой, чтобы она лучше читалась на фоне. За цвет обводки отвечает strokeStyle, за толщину линии — lineWidth, а рисует обводку команда stroke() вместо fill():
context.fillStyle = 'gold';
context.strokeStyle = '#a07c00';
context.lineWidth = 3;
context.beginPath();
context.arc(240, 180, 20, 0, Math.PI * 2);
context.fill(); // сначала залили золотым
context.stroke(); // потом обвели тёмной рамкойРезультат: та же золотая монетка, но теперь у неё аккуратная тёмно-жёлтая обводка толщиной 3 пикселя. Монетка выглядит объёмнее и заметнее на голубом небе.
Можно вызывать fill() и stroke() для одного контура подряд — сначала зальётся внутренность, потом нарисуется рамка по краю. Точно так же работают fillRect и парный ему strokeRect для прямоугольников.
Пример 3: главное — выводим спрайт цыплёнка
Фигуры — это здорово, но герой из квадратиков выглядит грустно. Настоящие игры рисуют объекты картинками — спрайтами. Команда для этого — context.drawImage(). Но есть нюанс, на котором спотыкаются вообще все: картинку нельзя нарисовать сразу. Сначала браузер должен её загрузить с диска или из сети, а это занимает время.
Метафора простая: ты заказал распечатку фото в фотосалоне. Пока фото не готово — клеить его в альбом бесполезно, на странице будет пусто. Поэтому мы создаём объект картинки, говорим ему адрес файла и ждём события onload — «фото готово». И только внутри этого обработчика рисуем спрайт. Заведём нашего сквозного героя:
const chickenSprite = new Image();
chickenSprite.src = '/sprites/chicken.png';
chickenSprite.onload = function () {
// картинка загрузилась — теперь её можно рисовать
context.drawImage(chickenSprite, 200, 250, 48, 48);
};Результат: как только файл спрайта подгрузится, на земле (примерно в координатах 200, 250) появляется цыплёнок размером 48×48 пикселей. Это и есть наш герой, который будет жить во всех следующих уроках курса.
Разберём по шагам:
new Image()— создаём пустой объект-картинку и кладём его в переменнуюchickenSprite. Это имя мы переиспользуем во всех уроках, так что запомни его.chickenSprite.src = '...'— задаём адрес файла. В этот момент браузер начинает загрузку в фоне.chickenSprite.onload = function () { ... }— говорим: «когда загрузится, выполни вот это». Внутри и вызываемdrawImage.drawImage(картинка, x, y, ширина, высота)— рисуем спрайт: его левый верхний угол в точке (200, 250), а сам он растянут до 48 на 48 пикселей.
У drawImage есть короткая форма без размеров — drawImage(chickenSprite, 200, 250) — тогда картинка рисуется в свою натуральную величину. Но в играх почти всегда задают размер явно, чтобы герой не оказался гигантским или микроскопическим.
Пример 4: собираем всю сцену вместе
Теперь склеим всё в одну картинку: небо, землю, монетку с обводкой и цыплёнка. Порядок рисования = порядок слоёв, поэтому фон идёт первым, герой — последним. А поскольку спрайт грузится с задержкой, фон и фигуры рисуем сразу, а героя — внутри onload:
const canvas = document.getElementById('game');
const context = canvas.getContext('2d');
// 1. небо
context.fillStyle = 'skyblue';
context.fillRect(0, 0, 480, 360);
// 2. земля
context.fillStyle = 'forestgreen';
context.fillRect(0, 300, 480, 60);
// 3. монетка с обводкой
context.fillStyle = 'gold';
context.strokeStyle = '#a07c00';
context.lineWidth = 3;
context.beginPath();
context.arc(360, 180, 16, 0, Math.PI * 2);
context.fill();
context.stroke();
// 4. цыплёнок-спрайт (когда загрузится)
const chickenSprite = new Image();
chickenSprite.src = '/sprites/chicken.png';
chickenSprite.onload = function () {
context.drawImage(chickenSprite, 80, 256, 48, 48);
};Результат: на холсте готовая сцена платформера — голубое небо, зелёная земля внизу, золотая монетка с тёмной рамкой справа в воздухе, и цыплёнок, стоящий на земле слева. Это первый кадр твоей будущей игры.
Поздравляю — ты только что нарисовал статичный кадр игры из всех базовых кирпичиков: прямоугольников, круга и спрайта. В следующих уроках мы заставим этого цыплёнка двигаться, но рисуется он всегда вот этими же командами.
Заметь, как естественно код делится на части по комментариям: «небо», «земля», «монетка», «цыплёнок». Это не случайно — так пишут игры по-настоящему. Сначала ты рисуешь дальний фон, потом то, что ближе, потом героя поверх всего. Когда позже мы добавим движение, каждый кадр будет перерисовывать ровно эти же четыре блока заново, просто с новыми координатами. Так что присмотрись к структуре: ты смотришь на скелет будущей игры, а не на разовый набросок.
Частые ошибки и подводные камни
Вот на чём чаще всего спотыкаются, когда впервые рисуют на canvas. Пробеги глазами заранее — сэкономишь себе полчаса нервов.
- Рисуешь картинку сразу, без
onload. Самая частая ошибка. Если вызватьdrawImageсразу послеsrc, картинка ещё не успела загрузиться и на экране ничего не появится — ошибки в консоли тоже не будет, просто пусто. Всегда рисуй спрайт внутри обработчикаonload. - Забыл
beginPath()перед кругом. Если рисовать несколько дуг безbeginPath(), они склеиваются в один контур, и при заливке между ними появляются странные линии. Каждую новую фигуру с дугой начинай сbeginPath(). - Путаешь координаты прямоугольника и круга. У
fillRectточка(x, y)— это левый верхний угол, а уarc— центр круга. Если монетка оказалась не там, где ждал, дело почти всегда в этом. - Ждёшь, что ось
yидёт вверх. На canvasyрастёт вниз. Чтобы поставить объект ниже, увеличивайy, а не уменьшай. Земля у нас наy = 300именно потому, что это низ экрана. - Цвет «не меняется».
fillStyle— это кисточка: она держит последний выбранный цвет, пока ты её не перемакнёшь. Если две фигуры вышли одного цвета — ты просто забыл сменитьfillStyleперед второй. - Неправильный путь к спрайту. Если в
srcопечатка или файла нет, картинка не загрузится иonloadне сработает. Проверь адрес и срабатываниеonerror, если цыплёнок упорно не появляется.
Мини-практика: построй свою площадку
Теперь твоя очередь. Возьми код из четвёртого примера за основу и доработай сцену так, чтобы она стала «живее»:
- Добавь солнце — жёлтый круг в правом верхнем углу неба (подсказка: центр где-то в районе
x = 420, y = 50, радиус 30). - Нарисуй вторую платформу — узкий коричневый прямоугольник, парящий над землёй, на который цыплёнок потом сможет запрыгнуть.
- Поставь три монетки в ряд над платформой. Совет: чтобы не копировать код трижды, нарисуй их в цикле
for, меняя только координатуx. - Передвинь цыплёнка так, чтобы он стоял на краю первой платформы, а не на земле.
Если всё получилось — у тебя на экране полноценный кадр уровня, собранный своими руками. Сохрани этот код: имена chickenSprite и context мы понесём дальше без изменений.
Итоги и что дальше
Сегодня ты освоил базовый набор команд рисования, на котором держится любая игровая графика:
fillRect(x, y, w, h)— прямоугольник по углу;beginPath()+arc(x, y, r, 0, Math.PI * 2)+fill()— круг по центру;fillStyleиstrokeStyle+lineWidth+stroke()— заливка и обводка;new Image()+onload+drawImage()— вывод спрайта цыплёнка.
И запомнил три вещи, об которые спотыкаются все: ось y идёт вниз, спрайт рисуют только после onload, а порядок команд = порядок слоёв.
Но пока наш цыплёнок стоит как вкопанный — это всего один кадр. В следующем уроке мы заставим картинку обновляться много раз в секунду: познакомимся с игровым циклом и функцией requestAnimationFrame, которая задаёт ритм игры, как сердцебиение. Тогда цыплёнок впервые задвигается по экрану. Увидимся там.