Разработка компьютерной игры — это отличный способ узнать больше о Qt5. Поэтому сегодня мы попробуем создать свою версию известной аркадной игры под названием «Арканоид» (англ. «Breakout»), разработанной в 1976 году не менее известной компанией Atari Inc. В этой игре игрок перемещает подвижную ракетку только вправо или влево и пытается отбросить летящий к нему мяч в верхнюю часть экрана, где расположена группа кирпичей/блоков. Цель состоит в том, чтобы, отбивая мяч, уничтожить все блоки.
Разработка игры «Арканоид»
Для начала давайте перечислим основные моменты игры. У нас есть одна подвижная Ракетка, один Мяч и тридцать Кирпичей/Блоков. Еще нам потребуется таймер, который будет использоваться для создания игрового цикла. Чтобы сделать проект как можно проще и понятнее, мы не будем рассматривать вопросы реализации начисления игровых очков, бонусов и пр.
Итак, начнем с заголовочного файла для объекта paddle
(подвижная Ракетка). INITIAL_X
и INITIAL_Y
— это константы, которые представляют начальные координаты объекта paddle
.
Заголовочный файл — paddle.h:
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 |
#pragma once #include <QImage> #include <QRect> class Paddle { public: Paddle(); ~Paddle(); public: void resetState(); void move(); void setDx(int); QRect getRect(); QImage & getImage(); private: QImage image; QRect rect; int dx; static const int INITIAL_X = 200; static const int INITIAL_Y = 360; }; |
Как уже было упомянуто, Ракетка может перемещаться только вправо или влево.
Файл реализации — paddle.cpp:
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 |
#include <iostream> #include "paddle.h" Paddle::Paddle() { dx = 0; image.load("paddle.png"); rect = image.rect(); resetState(); } Paddle::~Paddle() { std::cout << ("Paddle deleted") << std::endl; } void Paddle::setDx(int x) { dx = x; } void Paddle::move() { int x = rect.x() + dx; int y = rect.top(); rect.moveTo(x, y); } void Paddle::resetState() { rect.moveTo(INITIAL_X, INITIAL_Y); } QRect Paddle::getRect() { return rect; } QImage & Paddle::getImage() { return image; } |
В конструкторе класса Paddle мы инициируем переменную dx
и загружаем изображение Ракетки. Далее получаем прямоугольник, ограничивающий загруженное изображение Ракетки, и устанавливаем его в исходное положение:
1 2 3 4 5 6 7 |
Paddle::Paddle() { dx = 0; image.load("paddle.png"); rect = image.rect(); resetState(); } |
Метод move() перемещает прямоугольник с изображением Ракетки. Направление движения задается переменной dx
:
1 2 3 4 5 6 |
void Paddle::move() { int x = rect.x() + dx; int y = rect.top(); rect.moveTo(x, y); } |
Функция resetState() устанавливает Ракетку в исходное положение:
1 2 3 |
void Paddle::resetState() { rect.moveTo(INITIAL_X, INITIAL_Y); } |
Теперь рассмотрим заголовочный файл для объекта brick
(Кирпич/Блок). Если Кирпич разрушен, то значением переменной destroyed
становится true
.
Заголовочный файл — brick.h:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
#pragma once #include <QImage> #include <QRect> class Brick { public: Brick(int, int); ~Brick(); public: bool isDestroyed(); void setDestroyed(bool); QRect getRect(); void setRect(QRect); QImage & getImage(); private: QImage image; QRect rect; bool destroyed; }; |
Класс Brick
представляет объект brick
.
Файл реализации — brick.cpp:
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 |
#include <iostream> #include "brick.h" Brick::Brick(int x, int y) { image.load("brickie.png"); destroyed = false; rect = image.rect(); rect.translate(x, y); } Brick::~Brick() { std::cout << ("Brick deleted") << std::endl; } QRect Brick::getRect() { return rect; } void Brick::setRect(QRect rct) { rect = rct; } QImage & Brick::getImage() { return image; } bool Brick::isDestroyed() { return destroyed; } void Brick::setDestroyed(bool destr) { destroyed = destr; } |
Конструктор класса Brick загружает изображение Кирпича, инициирует переменную-флаг destroyed
и устанавливает изображение в исходную позицию:
1 2 3 4 5 6 |
Brick::Brick(int x, int y) { image.load("brickie.png"); destroyed = false; rect = image.rect(); rect.translate(x, y); } |
У объектов класса Brick есть переменная-флаг destroyed
. Если её значение установлено как true
, то Кирпич считается разрушенным, и не отображается в окне:
1 2 3 |
bool Brick::isDestroyed() { return destroyed; } |
Переходим к заголовочному файлу объекта ball
(Мяч). В переменных xdir
и ydir
хранится направление движения Мяча.
Заголовочный файл — ball.h:
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 |
#pragma once #include <QImage> #include <QRect> class Ball { public: Ball(); ~Ball(); public: void resetState(); void autoMove(); void setXDir(int); void setYDir(int); int getXDir(); int getYDir(); QRect getRect(); QImage & getImage(); private: int xdir; int ydir; QImage image; QRect rect; static const int INITIAL_X = 230; static const int INITIAL_Y = 355; static const int RIGHT_EDGE = 300; }; |
В начале игры Мяч движется в направлении вправо-вверх:
1 2 |
xdir = 1; ydir = -1; |
Метод autoMove() вызывается в каждом игровом цикле для перемещения Мяча по экрану. Если Мяч достигает границ окна (за исключением нижней), то он меняет свое направление. Если же Мяч попадает в нижнюю границу окна, то назад он не отскакивает, а игра при этом считается завершенной:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
void Ball::autoMove() { rect.translate(xdir, ydir); if (rect.left() == 0) { xdir = 1; } if (rect.right() == RIGHT_EDGE) { xdir = -1; } if (rect.top() == 0) { ydir = 1; } } |
Заголовочный файл — breakout.h:
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 |
#pragma once #include <QWidget> #include <QKeyEvent> #include "ball.h" #include "brick.h" #include "paddle.h" class Breakout : public QWidget { public: Breakout(QWidget *parent = 0); ~Breakout(); protected: void paintEvent(QPaintEvent *); void timerEvent(QTimerEvent *); void keyPressEvent(QKeyEvent *); void keyReleaseEvent(QKeyEvent *); void drawObjects(QPainter *); void finishGame(QPainter *, QString); void moveObjects(); void startGame(); void pauseGame(); void stopGame(); void victory(); void checkCollision(); private: int x; int timerId; static const int N_OF_BRICKS = 30; static const int DELAY = 10; static const int BOTTOM_EDGE = 400; Ball *ball; Paddle *paddle; Brick *bricks[N_OF_BRICKS]; bool gameOver; bool gameWon; bool gameStarted; bool paused; }; |
Ракетка управляется с помощью клавиш-стрелок на клавиатуре. В игре мы следим за событиями нажатия клавиш клавиатуры с помощью следующих методов:
1 2 |
void keyPressEvent(QKeyEvent *); void keyReleaseEvent(QKeyEvent *); |
В переменной x
хранится текущее положение Ракетки по оси X
. Переменная timerId
используется для идентификации объекта timer
. Это необходимо в тех моментах, когда мы приостанавливаем игру:
1 2 |
int x; int timerId; |
Константа N_OF_BRICKS
задает количество Кирпичей в игре:
1 |
static const int N_OF_BRICKS = 30; |
Константа DELAY
управляет скоростью игры:
1 |
static const int DELAY = 10; |
Когда Мяч пересекает нижний край окна, то игра заканчивается:
1 |
static const int BOTTOM_EDGE = 400; |
Переменные-указатели на объекты Мяча, Ракетки и массива Кирпичей:
1 2 3 |
Ball *ball; Paddle *paddle; Brick *bricks[N_OF_BRICKS]; |
Следующие 4 переменные отвечают за различные состояния игры:
1 2 3 4 |
bool gameOver; bool gameWon; bool gameStarted; bool paused; |
В файле breakout.cpp находится логика нашей игры.
Файл реализации — breakout.cpp:
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 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 |
#include <QPainter> #include <QApplication> #include "breakout.h" Breakout::Breakout(QWidget *parent) : QWidget(parent) { x = 0; gameOver = false; gameWon = false; paused = false; gameStarted = false; ball = new Ball(); paddle = new Paddle(); int k = 0; for (int i=0; i<5; i++) { for (int j=0; j<6; j++) { bricks[k] = new Brick(j*40+30, i*10+50); k++; } } } Breakout::~Breakout() { delete ball; delete paddle; for (int i=0; i<N_OF_BRICKS; i++) { delete bricks[i]; } } void Breakout::paintEvent(QPaintEvent *e) { Q_UNUSED(e); QPainter painter(this); if (gameOver) { finishGame(&painter, "Game lost"); } else if(gameWon) { finishGame(&painter, "Victory"); } else { drawObjects(&painter); } } void Breakout::finishGame(QPainter *painter, QString message) { QFont font("Courier", 15, QFont::DemiBold); QFontMetrics fm(font); int textWidth = fm.width(message); painter->setFont(font); int h = height(); int w = width(); painter->translate(QPoint(w/2, h/2)); painter->drawText(-textWidth/2, 0, message); } void Breakout::drawObjects(QPainter *painter) { painter->drawImage(ball->getRect(), ball->getImage()); painter->drawImage(paddle->getRect(), paddle->getImage()); for (int i=0; i<N_OF_BRICKS; i++) { if (!bricks[i]->isDestroyed()) { painter->drawImage(bricks[i]->getRect(), bricks[i]->getImage()); } } } void Breakout::timerEvent(QTimerEvent *e) { Q_UNUSED(e); moveObjects(); checkCollision(); repaint(); } void Breakout::moveObjects() { ball->autoMove(); paddle->move(); } void Breakout::keyReleaseEvent(QKeyEvent *e) { int dx = 0; switch (e->key()) { case Qt::Key_Left: dx = 0; paddle->setDx(dx); break; case Qt::Key_Right: dx = 0; paddle->setDx(dx); break; } } void Breakout::keyPressEvent(QKeyEvent *e) { int dx = 0; switch (e->key()) { case Qt::Key_Left: dx = -1; paddle->setDx(dx); break; case Qt::Key_Right: dx = 1; paddle->setDx(dx); break; case Qt::Key_P: pauseGame(); break; case Qt::Key_Space: startGame(); break; case Qt::Key_Escape: qApp->exit(); break; default: QWidget::keyPressEvent(e); } } void Breakout::startGame() { if (!gameStarted) { ball->resetState(); paddle->resetState(); for (int i=0; i<N_OF_BRICKS; i++) { bricks[i]->setDestroyed(false); } gameOver = false; gameWon = false; gameStarted = true; timerId = startTimer(DELAY); } } void Breakout::pauseGame() { if (paused) { timerId = startTimer(DELAY); paused = false; } else { paused = true; killTimer(timerId); } } void Breakout::stopGame() { killTimer(timerId); gameOver = true; gameStarted = false; } void Breakout::victory() { killTimer(timerId); gameWon = true; gameStarted = false; } void Breakout::checkCollision() { if (ball->getRect().bottom() > BOTTOM_EDGE) { stopGame(); } for (int i=0, j=0; i<N_OF_BRICKS; i++) { if (bricks[i]->isDestroyed()) { j++; } if (j == N_OF_BRICKS) { victory(); } } if ((ball->getRect()).intersects(paddle->getRect())) { int paddleLPos = paddle->getRect().left(); int ballLPos = ball->getRect().left(); int first = paddleLPos + 8; int second = paddleLPos + 16; int third = paddleLPos + 24; int fourth = paddleLPos + 32; if (ballLPos < first) { ball->setXDir(-1); ball->setYDir(-1); } if (ballLPos >= first && ballLPos < second) { ball->setXDir(-1); ball->setYDir(-1*ball->getYDir()); } if (ballLPos >= second && ballLPos < third) { ball->setXDir(0); ball->setYDir(-1); } if (ballLPos >= third && ballLPos < fourth) { ball->setXDir(1); ball->setYDir(-1*ball->getYDir()); } if (ballLPos > fourth) { ball->setXDir(1); ball->setYDir(-1); } } for (int i=0; i<N_OF_BRICKS; i++) { if ((ball->getRect()).intersects(bricks[i]->getRect())) { int ballLeft = ball->getRect().left(); int ballHeight = ball->getRect().height(); int ballWidth = ball->getRect().width(); int ballTop = ball->getRect().top(); QPoint pointRight(ballLeft + ballWidth + 1, ballTop); QPoint pointLeft(ballLeft - 1, ballTop); QPoint pointTop(ballLeft, ballTop -1); QPoint pointBottom(ballLeft, ballTop + ballHeight + 1); if (!bricks[i]->isDestroyed()) { if(bricks[i]->getRect().contains(pointRight)) { ball->setXDir(-1); } else if(bricks[i]->getRect().contains(pointLeft)) { ball->setXDir(1); } if(bricks[i]->getRect().contains(pointTop)) { ball->setYDir(1); } else if(bricks[i]->getRect().contains(pointBottom)) { ball->setYDir(-1); } bricks[i]->setDestroyed(true); } } } } |
В конструкторе класса Breakout мы создаем экземпляр тридцати Кирпичей:
1 2 3 4 5 6 7 |
int k = 0; for (int i=0; i<5; i++) { for (int j=0; j<6; j++) { bricks[k] = new Brick(j*40+30, i*10+50); k++; } } |
В зависимости от переменных gameOver
и gameWon
мы либо заканчиваем игру, выдавая соответствующие сообщения, либо продолжаем отрисовывать в окне игровые объекты:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
void Breakout::paintEvent(QPaintEvent *e) { Q_UNUSED(e); QPainter painter(this); if (gameOver) { finishGame(&painter, "Game lost"); } else if(gameWon) { finishGame(&painter, "Victory"); } else { drawObjects(&painter); } } |
Метод finishGame() отображает завершающее сообщение в центре окна. Это либо "Game lost"
("Игра проиграна"
), либо "Victory"
("Победа"
). Метод QFontMetrics::width() используется для вычисления ширины строки соответствующего сообщения:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
void Breakout::finishGame(QPainter *painter, QString message) { QFont font("Courier", 15, QFont::DemiBold); QFontMetrics fm(font); int textWidth = fm.width(message); painter->setFont(font); int h = height(); int w = width(); painter->translate(QPoint(w/2, h/2)); painter->drawText(-textWidth/2, 0, message); } |
Метод drawObjects() отрисовывает в окне все объекты игры: Мяч, Ракетку и Кирпичи. А так как данные объекты представлены изображениями, то при помощи метода drawImage() мы отображаем и их изображения:
1 2 3 4 5 6 7 8 9 10 11 |
void Breakout::drawObjects(QPainter *painter) { painter->drawImage(ball->getRect(), ball->getImage()); painter->drawImage(paddle->getRect(), paddle->getImage()); for (int i=0; i<N_OF_BRICKS; i++) { if (!bricks[i]->isDestroyed()) { painter->drawImage(bricks[i]->getRect(), bricks[i]->getImage()); } } } |
В теле метода timerEvent() мы сначала перемещаем объекты, а затем проверяем, не столкнулся ли Мяч с Ракеткой или Кирпичом. В конце генерируем событие отрисовки:
1 2 3 4 5 6 7 8 |
void Breakout::timerEvent(QTimerEvent *e) { Q_UNUSED(e); moveObjects(); checkCollision(); repaint(); } |
Метод moveObjects() отвечает за перемещения объектов Мяч и Ракетка. В нем вызываются их собственные методы перемещения:
1 2 3 4 5 |
void Breakout::moveObjects() { ball->autoMove(); paddle->move(); } |
Когда игрок отпускает кнопку ←
или →
, то мы присваиваем переменной dx
Ракетки значение 0
. В результате Ракетка перестает двигаться:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
void Breakout::keyReleaseEvent(QKeyEvent *e) { int dx = 0; switch (e->key()) { case Qt::Key_Left: dx = 0; paddle->setDx(dx); break; case Qt::Key_Right: dx = 0; paddle->setDx(dx); break; } } |
В методе keyPressEvent() мы отслеживаем события нажатия клавиш, относящиеся к нашей игре. Кнопки ←
и →
перемещают объект Ракетки. Они влияют на значение переменной dx
, которое затем будет добавлено к координате х
самой Ракетки. Кнопка P
ставит игру на паузу, кнопка "Пробел"
запускает игру, а кнопка Esc
завершает работу приложения:
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 |
void Breakout::keyPressEvent(QKeyEvent *e) { int dx = 0; switch (e->key()) { case Qt::Key_Left: dx = -1; paddle->setDx(dx); break; case Qt::Key_Right: dx = 1; paddle->setDx(dx); break; case Qt::Key_P: pauseGame(); break; case Qt::Key_Space: startGame(); break; case Qt::Key_Escape: qApp->exit(); break; default: QWidget::keyPressEvent(e); } } |
Метод startGame() сбрасывает состояния объектов ball
и paddle
; они перемещаются в исходное положение. В цикле for мы устанавливаем значение флага destroyed
равным false
для каждого Кирпича, таким образом отображая их в окне. Переменные gameOver
, gameWon
и gameStarted
получают свои начальные логические значения. Наконец, с помощью метода startTimer() мы запускаем таймер:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
void Breakout::startGame() { if (!gameStarted) { ball->resetState(); paddle->resetState(); for (int i=0; i<N_OF_BRICKS; i++) { bricks[i]->setDestroyed(false); } gameOver = false; gameWon = false; gameStarted = true; timerId = startTimer(DELAY); } } |
Функция pauseGame() используется для приостановки и запуска уже остановленной игры. Данное состояние игры управляется с помощью переменной paused
. Также мы храним и идентификатор таймера. Чтобы приостановить игру, мы «убиваем» таймер с помощью метода killTimer(). Чтобы перезапустить его, мы вызываем метод startTimer():
1 2 3 4 5 6 7 8 9 10 11 12 |
void Breakout::pauseGame() { if (paused) { timerId = startTimer(DELAY); paused = false; } else { paused = true; killTimer(timerId); } } |
В методе stopGame() мы «убиваем» таймер и устанавливаем соответствующие флаги:
1 2 3 4 5 6 |
void Breakout::stopGame() { killTimer(timerId); gameOver = true; gameStarted = false; } |
В методе checkCollision() мы делаем проверку столкновения Мяча с игровыми объектами. Игра заканчивается, если Мяч попадает в нижний край окна:
1 2 3 4 5 6 7 |
void Breakout::checkCollision() { if (ball->getRect().bottom() > BOTTOM_EDGE) { stopGame(); } ... } |
Проверяем количество разрушенных Кирпичей. Если все Кирпичи уничтожены, то мы выиграли:
1 2 3 4 5 6 7 8 9 10 |
for (int i=0, j=0; i<N_OF_BRICKS; i++) { if (bricks[i]->isDestroyed()) { j++; } if (j == N_OF_BRICKS) { victory(); } } |
Если Мяч попадает в верхнюю часть Ракетки, то меняем направление полета Мяча (в зависимости от движения Ракетки в момент столкновения), например, на влево-вверх:
1 2 3 4 |
if (ballLPos < first) { ball->setXDir(-1); ball->setYDir(-1); } |
Если Мяч ударяется о нижнюю часть Кирпича, то мы меняем направление y Мяча — он идет вниз.
1 2 3 |
if(bricks[i]->getRect().contains(pointTop)) { ball->setYDir(1); } |
Основной файл программы — main.cpp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
#include <QApplication> #include "breakout.h" int main(int argc, char *argv[]) { QApplication app(argc, argv); Breakout window; window.resize(300, 400); window.setWindowTitle("Breakout"); window.show(); return app.exec(); } |
Результатом является игра «Арканоид»:
GitHub / Урок №15. Создание игры «Арканоид» на С++/Qt5 — Исходный код
Заключение
Ну что же, вот и подошел к концу цикл уроков по Qt5. Начиная с самых азов, мы постепенно научились работать с объектами, такими как строки, контейнеры, дата и время, файлы и каталоги, сигналы, слоты и события, меню и панели инструментов. Рассмотрели вопросы управления компоновкой виджетов и даже создали пару интересных игр. Надеюсь, что каждый, кто прочитал эти статьи, смог найти для себя что-то новое, что-то интересное.
Моя версия на Qt6 в среде MS VS2022 Enterprise Edition:
Я не стал разделять мячик, ракетку и кирпич на отдельные классы, а сделал их одним типом данных — Item.
Item.h
Item.cpp — отсутствует, так как класс Item слишком простой.
Breakout.h
Breakout.cpp
main.cpp
Где ball.cpp ? ))
В ГитХаб-репозитории.
Подскажите пожалуйста, как скомпилировать всё статически или динамически, но чтобы готовый файл или проект можно было запустить на другой машине?
Вам нужна программа windeployqt.exe из поставки Qt. Обычно она располагается в папке с компилятором. К примеру, у меня это папки "C:\Soft\Qt\5.15.0\mingw81_64\bin" или "C:\Soft\Qt\5.15.0\msvc2019_64\bin". Находите её, далее запускаете командную строку -> прописываете пусть к этой программе -> пробел -> путь к исполняемому файлу вашей программы -> enter.
В результате этого, windeployqt.exe добавит все необходимые *.dll в папку с вашей программой.
Спасибо, попробую!
Спасибо Вам огромное!Вы святой
Аминь 🙂
Добрый день. Огромная благодарность автору, за сей сайт!
Хотелось бы продолжение но уже по QML.
Отличные уроки, только, кажется, в этой статье не хватает листинга ball.cpp
Да(Там ниже реализована только одна функция.Без этого работает?
А будут ли уроки QT по подключению базы данных к форме и их взаимодействию ?
Этот урок уже есть — «Создание форм регистрации и авторизации в Qt5/C++»
И еще почему код не в гит-репозитории? можно было бы изменять, дополнять и тд)
Тогда и статью нужно будет переместить в гит-репозиторий 🙂
Спасибо автору, очень интересно читать.
Хочется продолжения: модель-представлений, таблиц, QML, работы с потоками, может мобильная разработка?
Насчет продолжения — продолжение будет.
P.S.: Всегда пожалуйста. 🙂