Вот и подоспела вторая часть туториала по созданию тетриса на C++/SFML (первая часть здесь). Сегодня мы рассмотрим вопросы реализации механизма вращения фигурок тетрамино, а также их горизонтальные и вертикальные перемещения по игровому полю.
Горизонтальное перемещение тетрамино
Предлагаю начать с самой легкой части, а именно с реализации перемещения тетрамино вдоль горизонтали. Управлять данным действием мы будем при помощи стрелок на клавиатуре. Для этого нам понадобится добавить обработчики событий на нажатия соответствующих клавиш на клавиатуре:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
sprite.setTextureRect(IntRect(0, 0, 18, 18)); // Переменная для горизонтального перемещения тетрамино int dx = 0; while (window.pollEvent(event)) { // Пользователь нажал на «крестик» и хочет закрыть окно? if (event.type == Event::Closed) // тогда закрываем его window.close(); // Была ли нажата клавиша на клавиатуре? if (event.type == Event::KeyPressed) // Эта кнопка – стрелка вверх? // if (event.key.code == Keyboard::Up) rotate = true; // Или может стрелка влево? else if (event.key.code == Keyboard::Left) dx = -1; // Или стрелка вправо? else if (event.key.code == Keyboard::Right) dx = 1; } // Горизонтальное перемещение for (int i = 0; i < 4; i++) a[i].x += dx; int n = 3; // Первое появление тетрамино на поле? if(a[0].x == 0) for (int i = 0; i < 4; i++) { a[i].x = figures[n][i] % 2; a[i].y = figures[n][i] / 2; } dx = 0; |
Данный код очень простой. Сначала мы отлавливаем событие нажатия клавиши на клавиатуре (Event::KeyPressed
), а затем проверяем, была ли это «стрелка влево» (Keyboard::Left
) или «стрелка вправо» (Keyboard::Right
). В зависимости от нажатой клавиши мы делаем шаг влево (dx = -1
) или же шаг вправо (dx = 1
). Далее, при помощи цикла for, мы сдвигаем все части тетрамино в нужном направлении. И в конце мы присваиваем ноль для переменной, обозначающей горизонтальное перемещение (dx = 0;
).
Рассмотрим детально следующий код:
1 2 3 4 5 6 7 |
// Первое появление тетрамино на поле? if(a[0].x == 0) for (int i = 0; i < 4; i++) { a[i].x = figures[n][i] % 2; a[i].y = figures[n][i] / 2; } |
Здесь мы задаем первоначальные координаты для тетрамино. Строка if(a[0].x == 0)
является проверкой, убрав которую, мы будем постоянно затирать изменяющиеся в результате горизонтального перемещения координаты тетрамино первоначальными координатами. Исходя из этого наши фигуры будут постоянно отрисовываться в верхнем левом углу игрового поля. Но благодаря проверке, указанной выше, код, отвечающий за первоначальное размещение тетрамино на игровом поле, будет выполняться только тогда, когда наша фигура находится в «стартовом» положении, и будет пропускаться тогда, когда тетрамино совершило перемещение вдоль оси.
В результате у нас получится что-то вроде следующего:
Вращение тетрамино
Теперь мы переходим к чуть более сложной части нашего туториала. Как вы наверняка помните, в тетрисе, помимо перемещения фигурок тетрамино вдоль горизонтальной оси, фигурки можно вращать. Для реализации данного функционала нам понадобятся знания линейной алгебры, а именно то, что вращение спрайта вокруг заданной точки с координатами (x_0; y_0)
описывается уравнениями следующего вида:
X = x_0 + (x − x_0) * cos(a) − (y − y_0 ) * sin(a);
Y = y_0 + (y − y_0) * cos(a) + (x − x_0 ) * sin(a);
где (x; y)
— это старые (исходные) координаты точки, (X; Y)
— это новые координаты (после вращения), а а
— это угол поворота. Так как все повороты у нас идут исключительно на 90°, а из школьного курса алгебры мы знаем, что:
sin(90°) = 1
cos(90°) = 0
то, подставляя соответствующие значения синуса и косинуса, исходные уравнения упрощаются до следующего вида:
X = x_0 − (y − y_0);
Y = y_0 + (x − x_0);
Картинка для наглядности:
Примечание: Не забудьте раскомментировать следующую строку кода:
1 2 |
// Эта кнопка – стрелка вверх? if (event.key.code == Keyboard::Up) rotate = true; |
А теперь на практике:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
// Создание спрайта Sprite sprite(texture); // Вырезаем из спрайта отдельный квадратик размером 18х18 пикселей sprite.setTextureRect(IntRect(0, 0, 18, 18)); int dx = 0; // переменная для горизонтального перемещения тетрамино bool rotate = 0; // переменная для вращения тетрамино /********************ПРОПУЩЕНА ЧАСТЬ КОДА*********************************************/ // Горизонтальное перемещение for (int i = 0; i < 4; i++) a[i].x += dx; // Вращение if (rotate) { Point p = a[1]; // указываем центр вращения for (int i = 0; i < 4; i++) { int x = a[i].y - p.y; // y - y0 int y = a[i].x - p.x; // x - x0 a[i].x = p.x - x; a[i].y = p.y + y; } } int n = 3; // Первое появление тетрамино на поле? if(a[0].x==0) for (int i = 0; i < 4; i++) { a[i].x = figures[n][i] % 2; a[i].y = figures[n][i] / 2; } dx = 0; rotate = 0; // Отрисовка окна window.clear(Color::White); |
Результат выполнения программы:
Вертикальное перемещение тетрамино
Осталось реализовать падение тетрамино вниз. Для этого мы создадим таймер при помощи класса Clock, и по истечении 0.3 секунды с начала отсчета времени, мы будем сдвигать все части тетрамино на 1 позицию вниз. Для получения времени, прошедшего с момента старта таймера, мы будем использовать метод getElapsedTime(), а для перевода времени в секунды — метод asSeconds():
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
// Создание спрайта Sprite sprite(texture); // Вырезаем из спрайта отдельный квадратик размером 18х18 пикселей sprite.setTextureRect(IntRect(0, 0, 18, 18)); int dx = 0; // переменная для горизонтального перемещения тетрамино bool rotate = 0; // переменная для вращения тетрамино // Переменные для таймера и задержки float timer = 0, delay = 0.3; // Часы (таймер) Clock clock; // Главный цикл приложения выполняется, пока открыто окно while (window.isOpen()) { // Получаем время, прошедшее с начала отсчета, и конвертируем его в секунды float time = clock.getElapsedTime().asSeconds(); clock.restart(); timer += time; /********************ПРОПУЩЕНА ЧАСТЬ КОДА*********************************************/ if (rotate) { Point p = a[1]; // указываем центр вращения for (int i = 0; i < 4; i++) { int x = a[i].y - p.y; int y = a[i].x - p.x; a[i].x = p.x - x; a[i].y = p.y + y; } } // Движение тетрамино вниз («тик» таймера) if (timer > delay) { for (int i = 0; i < 4; i++) a[i].y += 1; timer = 0; } |
Как вы можете заметить, код довольно простой. В результате мы получим следующее:
GitHub / Часть №2: Создание игры «Тетрис» на C++/SFML — Исходный код
Заключение
Мы прошли уже больше половины пути к заветной цели. Осталось совсем немного: сделать рандомный выбор типа фигурок тетрамино, раскрасить их в разные цвета, реализовать механизм уничтожения при получении ровных линий из тетрамино, сделать возможным ускорение падения тетрамино, установить рамки игрового поля и красивую картинку в качестве фона. Всё это будет рассмотрено в следующей (заключительной) части нашего туториала.
Здравствуйте! Очень благодарен вам за уроки. Возник вопрос. Почему мы rotate пишем выше до заполнения a[i].
Мы же пользуемся a[i].x и a[i].y, но заполняем мы их позже.
Здравствуйте. Обьясните пожалуйста чем мы руководствуемся когда в части кода для вращения выставляем кординаты точки относительно которых происходит вращение на координаты а[1] тетрамино. Конкретно в этой строчке:
У нас тетрамино состоит всего из 4х спрайтов. Почему тогда именно второй из них мы выбираем центральным а не третий, или от этого ничего не меняется. Я пытаюсь это как то визуализировать но у меня не выходит. Хелп ми плиз)
Как гласит известная пословица: "Лучше 1 раз увидеть, чем 100 раз услышать". Попробуйте задать в качестве вращения не второй спрайт, а третий, и посмотреть, что из этого получится 🙂
Здравствуйте.
В первом куске кода, где мы проверяем, какие стрелки нажал пользователь, мы также проверяем стрелку вверх. Об этом нет пояснения в абзаце ниже. И если она нажата, то присваиваем неизвестной на тот момент булевой переменной rotate значение true. Нехорошо. Компилятор ругается, что rotate он не знает. Понятно, что в следующем куске кода rotate уже объясняется и инициализируется. Но в первом куске следует внести соответствующие комментарии или убрать часть про rotate.
Для тех, кому уравнения координат точки при вращении относительно заданного центра — как кантонский диалект китайского языка, можно было бы дать ссылку на годную статью или видео-объяснение по этой теме.
Спасибо за замечания. В ближайшее время мы внесем в статью необходимые коррективы. 🙂
Здравствуйте! Если я правильно всё понял — в ответ на предыдущий комментарий была закомментирована строчка if (event.key.code == Keyboard::Up …
После этого нужно заменить else if для следующего условия на if. Но тогда в следующей части урока придется снова менять на else if. Как Вам вариант сделать Keyboard::Up последним ?
Эта строчка попала в код раньше, чем должна была 🙂
Вы правильно подметили, что в таком случае нарушается структура "else if" и из-за этого требуется внести в код некоторые исправления, а именно — убрать первый else. Т.е., код:
// Или может стрелка влево?
else if (event.key.code == Keyboard::Left) dx = -1;
// Или стрелка вправо?
else if (event.key.code == Keyboard::Right) dx = 1;
заменить на:
// Или может стрелка влево?
if (event.key.code == Keyboard::Left) dx = -1;
// Или стрелка вправо?
else if (event.key.code == Keyboard::Right) dx = 1;
Спасибо, что напомнили об этом, постараюсь сегодня исправить этот недочет! 🙂
Странно. Нифига не получилось Скачал исходники — открыл — тоже не получилось =)
Пустое белое игровое поле..
>>"Пустое белое игровое поле.."
Только что проверил — всё работает. Скорее всего ваша проблема в том, что неправильно указан путь до текстуры. Обратите внимание на строчку:
У меня путь — это "C:\dev\SFML_Tutorial\Images\tiles.png"
Для вас 2 варианта решения:
1 Вариант.
Сделать путь относительным. Для этого замените строчку:
на
2. Вариант.
Распакуйте архив с исходниками. Найдите в нем папку:
"SFML — Tetris. Part2 — Sources"
Скопируйте всё её содержимое в "С:\dev\SFML_Tutorial" (предварительно создав на диске С: папку "dev", а в ней — папку "SFML_Tutorial"). Таким образом, картинки с текстурами должны будут лежать в "С:\dev\SFML_Tutorial\images"
С путем все ок.
Показывает тетрамино N=3 если закомментарю строку
Вот мой код если что
https://onlinegdb.com/ryid1CIoH
А зачем же вы все глобальные переменные перенесли внутрь функции main()? В особенности эти:
Ведь пока эти массивы структур были объявлены вне функции main() (т.е. как глобальные), то их элементы автоматически инициализировались нулями, благодаря чему проходилась проверка if (a[0].x == 0) и происходило отрисовывание тетрамино.
А вот когда вы эти массивы переместили внутрь функции main(), то они стали инициализироваться "мусором", отличным от нуля. Именно поэтому, проверка не проходила и тетрамино не отрисовывались. И именно же поэтому тетрамино снова отображались, когда вы закомментировали строчку с проверкой.
Картинка для наглядности:
https://ibb.co/0QbCGhY
Я немного по-другому сделала и работает без проверки нулевых координат:
Шикарно! Большое спасибо за ваш разбор:) В следующий раз, пожалуйста, разберите, как реализованы шахматы.
Пожалуйста. Постараюсь сделать и про шахматы 🙂
Огромное спасибо за разбор! все очень понятно и толково разъяснено. При возможности доработать и сделать более развернутую реализацию со всем функционалом прямо до готового продукта было бы очень круто! а еще было бы классно если показали похожую игру в ооп.
Всегда пожалуйста.
P.S.: Примем к сведению ваше пожелание 🙂