Вложенные циклы: сетки
Один цикл нарисовал тебе ряд цыплят. А что, если поставить цикл внутрь цикла — и получить целое поле, ряд за рядом?
Вложенный цикл — это цикл внутри другого цикла. Внешний отвечает за строки, внутренний — за столбцы. Вместе они проходят по каждой клетке двумерной сетки, как карандаш по клеточкам в тетради.
Зачем это вообще нужно
Открой любую игру с инвентарём — Minecraft, какой-нибудь тайл-карту или даже сетку иконок на рабочем столе телефона. Всё это сетки: ровные ряды клеток, идущие сверху вниз и слева направо. Шахматная доска — сетка. Пиксели на экране — огромная сетка. Лента постов в твоей соцсети, где фотки выстроены по три в ряд, — тоже сетка.
В прошлом уроке про цикл for и ряды ты научился рисовать одной командой целую цепочку цыплят в линию. Здорово, но это всего лишь одна строка. А что если нам нужно не пять цыплят в ряд, а пять рядов по пять — целое птичье поле, двадцать пять штук разом? Писать пять циклов подряд? Скучно и некрасиво. Есть приём изящнее.
К концу урока ты соберёшь скетч, который заполняет весь холст аккуратной сеткой маленьких CodeChick — ровными рядами и столбцами, как яйца в коробке. И всё это — буквально пара строк с двумя циклами for, вложенными друг в друга. Давай разбираться, как это работает.
Метафора: читаем книгу по клеточкам
Вспомни, как ты читаешь страницу книги. Ты идёшь по первой строке слева направо: слово, слово, слово — до конца строки. Дочитал строку — спускаешься на следующую и снова идёшь слева направо. И так строка за строкой, пока страница не кончится.
Вложенные циклы работают точно так же. Есть две роли:
- Внешний цикл — это переход на новую строку. Он медленный: один шаг внешнего цикла — это одна целая строка.
- Внутренний цикл — это движение глаз по словам внутри строки, слева направо. Он быстрый: успевает целиком пробежать всю строку, прежде чем внешний сделает один шаг.
Главное правило: внутренний цикл успевает полностью отработать на каждом шаге внешнего. Если внешний прошёл 5 строк, а внутренний на каждой строке делает 5 шагов, то всего получится 5 на 5 = 25 действий.
В тетради в клеточку это ещё нагляднее. Представь, что ты ставишь точку в каждой клетке. Номер строки — это сколько клеток вниз ты опустился, номер столбца — сколько вправо. Пара чисел (столбец, строка) однозначно показывает любую клетку. Эти два счётчика нам и дадут два вложенных цикла.
Ещё одна картинка из жизни, которая помогает почувствовать ритм вложенных циклов, — настольный календарь или расписание уроков на неделю. Сверху семь дней — это столбцы, слева номера уроков — это строки. Чтобы заполнить весь календарь, ты берёшь первую строку и проходишь по всем дням слева направо, потом вторую строку — снова по всем дням, и так далее. Заметь: дни (внутренний цикл) повторяются на каждой строке заново, а строки (внешний цикл) идут только вниз и без возврата. Ровно так же ведут себя col и row в коде: внутренний счётчик каждый раз сбрасывается в ноль и пробегает заново, а внешний неторопливо растёт.
И запомни ещё один важный момент: порядок вложения определяет, что мы перебираем «чаще». Раз внутренний цикл крутится быстрее, именно он отвечает за то, что меняется на каждом маленьком шаге. Если поменять циклы местами — сделать внешним столбцы, а внутренним строки, — сетка останется такой же ровной, но заполняться она будет уже по вертикали: сначала весь первый столбец сверху вниз, потом второй. Для статичной картинки разницы не видно, но как только мы начнём раскрашивать клетки по очереди или анимировать их появление, порядок станет заметен.
Пример 1. Самая первая сетка из точек
Начнём с минимума: просто расставим точки-кружки по сетке, чтобы увидеть саму решётку. Возьмём 5 столбцов и 5 строк, шаг между ними — 80 пикселей.
function setup() {
createCanvas(400, 400);
noStroke();
}
function draw() {
background(255, 245, 200);
fill(255, 140, 0);
for (let row = 0; row < 5; row++) {
for (let col = 0; col < 5; col++) {
let x = 40 + col * 80;
let y = 40 + row * 80;
ellipse(x, y, 30, 30);
}
}
}Результат: на тёплом кремовом холсте появляется ровная решётка из 25 оранжевых кружков — пять рядов по пять, расставленных через равные промежутки, как точки на листе в клеточку. Между соседними кружками одинаковые отступы и по горизонтали, и по вертикали. Картинка статична, но идеально ровная.
Что здесь происходит по шагам
- Внешний цикл
for (let row = 0; row < 5; row++)отвечает за строки. Переменнаяrowпробегает значения 0, 1, 2, 3, 4 — пять строк. - Внутри него внутренний цикл
for (let col = 0; col < 5; col++)отвечает за столбцы. Переменнаяcolна каждой строке заново пробегает 0, 1, 2, 3, 4. - Для каждой пары (col, row) мы считаем координаты:
x = 40 + col * 80иy = 40 + row * 80. Число 40 — это отступ от края, а* 80раздвигает кружки на равное расстояние. - Команда
ellipse(x, y, 30, 30)рисует кружок в вычисленной клетке.
Прокрути в голове первые шаги. Внешний цикл ставит row = 0. Заходим во внутренний: col = 0 → точка слева вверху; col = 1 → правее; ... col = 4 → конец первой строки. Внутренний выдохся, управление возвращается наружу: row = 1 — спускаемся на строку ниже, и внутренний снова бежит слева направо. Ровно как чтение книги.
Полезно один раз честно посчитать, сколько раз вообще вызывается ellipse. Внешний цикл делает 5 шагов, и на каждом из них внутренний делает ещё 5 — получается 5 умножить на 5, то есть 25 кружков. Именно столько ты и видишь на экране. Если поменять оба числа на 10, кружков станет уже 100, а команда рисования в коде по-прежнему написана всего один раз. В этом и волшебство вложенных циклов: одна строчка ellipse(x, y, 30, 30) при двух циклах вокруг неё превращается в сотни фигур, и тебе не нужно копировать её руками.
Обрати внимание ещё на одну деталь: переменная col объявлена прямо в заголовке внутреннего цикла через let. Это значит, что она рождается заново при каждом запуске внутреннего цикла и не мешает внешней row. Эти две переменные живут в разных «слоях» и не путаются друг с другом — поэтому им так важно иметь разные имена.
Пример 2. Считаем координаты клетки честно
В первом примере числа 40 и 80 я взял почти на глаз. Давай сделаем по-взрослому: зададим, сколько у нас столбцов и строк, и пусть программа сама вычислит шаг, чтобы сетка точно вписалась в холст. Так ты сможешь поменять одно число — и сетка перестроится сама.
let cols = 6;
let rows = 6;
function setup() {
createCanvas(420, 420);
noStroke();
}
function draw() {
background(220, 240, 255);
let stepX = width / cols;
let stepY = height / rows;
for (let row = 0; row < rows; row++) {
for (let col = 0; col < cols; col++) {
// центр клетки, а не её левый угол
let x = col * stepX + stepX / 2;
let y = row * stepY + stepY / 2;
fill(255, 215, 0);
ellipse(x, y, stepX * 0.6, stepY * 0.6);
}
}
}Результат: на светло-голубом фоне выстроилась сетка 6 на 6 из жёлтых кружков — 36 штук. Каждый кружок аккуратно сидит ровно по центру своей клетки, и между ними остаются одинаковые поля. Если поменять cols и rows на 4, кружков станет меньше, но они станут крупнее и снова идеально заполнят холст.
Откуда берётся формула координат
Тут две важные мысли. Первая — про шаг. width / cols делит ширину холста на число столбцов и говорит, сколько пикселей приходится на одну клетку по горизонтали. Если холст 420 и столбцов 6, то stepX равен 70: каждая клетка ровно 70 пикселей шириной.
Вторая мысль — про центр клетки. col * stepX даёт левый край клетки. Если поставить кружок туда, он прилипнет к границе. Чтобы попасть в середину клетки, добавляем половину шага: + stepX / 2. Тот же приём по вертикали. Запомни эту формулу — индекс * шаг + шаг / 2 — она пригодится всякий раз, когда нужно расставить что-то по центрам ячеек.
Почему мы вычисляем шаг, а не вписываем число руками? Потому что так код становится гибким. В первом примере, если бы ты захотел сделать сетку плотнее, пришлось бы вручную пересчитывать и отступ 40, и шаг 80 — легко ошибиться. Здесь же всё держится на двух переменных cols и rows в самом верху файла. Поменял шестёрку на десятку — и программа сама поделит холст по-новому, сама подвинет кружки и сама подберёт им размер через stepX * 0.6. Это называется «параметризация»: ты выносишь главные числа наверх и управляешь всей картиной из одного места. Привыкай так делать — это отличает аккуратный код от каши из магических чисел, разбросанных по всему скетчу.
Кстати, размер кружка stepX * 0.6 тоже не случаен. Мы берём не весь шаг, а лишь 60 процентов от него — так между соседними кружками остаётся воздух, и сетка дышит. Поставь множитель 1.0 — кружки начнут касаться друг друга, а при 1.2 уже налезут. Поиграй с этим числом: оно управляет «плотностью» твоей сетки.
Пример 3. Поле цыплят
Пора оживить героя. Заменим скучный кружок на маленького CodeChick и заполним им всю сетку. Чтобы не дублировать десяток строк рисования внутри циклов, вынесем рисование одного цыплёнка в отдельную функцию chick(x, y, d), где d — его размер.
let cols = 5;
let rows = 4;
function setup() {
createCanvas(500, 400);
noStroke();
}
function draw() {
background(180, 225, 180);
let stepX = width / cols;
let stepY = height / rows;
for (let row = 0; row < rows; row++) {
for (let col = 0; col < cols; col++) {
let x = col * stepX + stepX / 2;
let y = row * stepY + stepY / 2;
chick(x, y, stepX * 0.5);
}
}
}
function chick(x, y, d) {
// тело
fill(255, 215, 0);
ellipse(x, y, d, d);
// клюв
fill(255, 140, 0);
triangle(x + d * 0.35, y, x + d * 0.6, y - d * 0.07, x + d * 0.6, y + d * 0.07);
// глаз
fill(0);
ellipse(x + d * 0.12, y - d * 0.12, d * 0.12, d * 0.12);
}Результат: на травянисто-зелёном поле выстроились 20 жёлтых цыплят — 5 в ряд, 4 ряда. У каждого свой оранжевый клювик, смотрящий вправо, и маленький чёрный глаз. Все одинакового размера и стоят строго по сетке, как стая, замершая по команде. Меняешь cols или rows — стая мгновенно перестраивается.
Почему рисование вынесено в функцию
Заметь главный трюк: внутри двух циклов всего одна строка — chick(x, y, stepX * 0.5). Вся возня с телом, клювом и глазом спрятана в функции chick. Это делает код циклов чистым и читаемым: внешний и внутренний цикл занимаются только расстановкой, а как выглядит один цыплёнок — отдельная забота. Если завтра захочешь добавить цыплёнку лапки, поправишь функцию в одном месте — и все 20 цыплят сразу обзаведутся лапками. Это и есть сила разделения «где» и «что».
Частые ошибки и подводные камни
1. Используют одну и ту же переменную в обоих циклах
Если по невнимательности написать for (let i...) и внутри тоже for (let i...), циклы начнут мешать друг другу — внутренний перезапишет счётчик внешнего, и всё сломается. Давай циклам разные имена: row и col (или хотя бы i и j). Понятные имена ещё и читаются яснее.
2. Путают, какой цикл строки, а какой столбцы
Внешний цикл — это строки (движение вниз, координата y), внутренний — столбцы (движение вправо, координата x). Если перепутать и умножить col на шаг по y, сетка вытянется не туда или ляжет боком. Держи в голове картинку чтения книги: сначала целая строка слева направо, потом переход вниз.
3. Забывают про вложенность и пишут циклы рядом
Два цикла друг за другом (сначала один for, потом закрыли его и начали второй) — это не сетка, а две отдельные линии. Чтобы получить решётку, второй for должен стоять внутри фигурных скобок первого. Следи за скобками: тело внешнего цикла целиком оборачивает внутренний.
4. Ставят знак >= или > вместо <
В условии col < cols используется строгое «меньше». Если случайно написать col <= cols, цикл сделает на один шаг больше — нарисует лишний столбец, который вылезет за край холста. Помни: при cols = 5 индексы идут 0, 1, 2, 3, 4 — это уже пять штук, и условие < 5 ровно их и даёт.
5. Считают координату без учёта индекса
Если внутри циклов написать просто ellipse(x, y, ...) с одними и теми же x и y, все фигуры лягут в одну точку друг на друга — ты увидишь один кружок вместо двадцати. Координаты обязаны зависеть от col и row: только умножение индекса на шаг разводит фигуры по разным клеткам.
Мини-проект: шахматное поле цыплят
Сделай сетку чуть умнее — пусть клетки чередуются по цвету, как на шахматной доске, а цыплята садятся только на светлые. База — пример 3, доделай сам:
- Заведи сетку 8 на 8 через переменные
colsиrows. - Внутри циклов сначала рисуй квадрат клетки командой
rect(col * stepX, row * stepY, stepX, stepY). - Цвет квадрата выбирай по чётности суммы индексов: если
(col + row) % 2 === 0— светлый, иначе тёмный. Это классический приём шахматной раскраски. - На светлые клетки сажай маленького
chick(...), на тёмные — ничего. - Усложни: сделай так, чтобы клюв цыплёнка смотрел в случайную сторону, или добавь зерно (маленький коричневый кружок) на тёмные клетки.
Когда заработает — поиграй с размером сетки и цветами. Попробуй очень мелкую сетку 20 на 20 из крошечных точек — получится почти текстура. Это твоё поле, экспериментируй.
Итоги
Сегодня ты освоил один из самых мощных приёмов в творческом коде — цикл внутри цикла. Вот что стоит унести с собой:
- Вложенный цикл = цикл внутри цикла. Внешний считает строки, внутренний — столбцы, ровно как чтение книги: строка за строкой, слева направо.
- Внутренний цикл полностью отрабатывает на каждом шаге внешнего. 5 строк по 5 столбцов = 25 фигур.
- Координаты клетки берутся из индексов:
x = col * шаг + шаг / 2,y = row * шаг + шаг / 2. Половина шага ставит фигуру в центр клетки. - Давай счётчикам разные имена (
col,row) и следи, чтобы внутренний цикл стоял именно внутри скобок внешнего.
Сетки — это фундамент очень многого: пиксель-арт, тайл-карты игр, мозаики, таблицы, поля для пятнашек и крестиков-ноликов. Стоит понять вложенные циклы — и половина генеративной графики становится тебе по плечу. В следующем уроке мы оживим эту сетку: научимся менять каждую клетку в зависимости от её места — делать волны, градиенты и узоры, где размер и цвет цыплёнка плавно меняются по полю. До встречи в коде!