Trong phần này chúng ta viết lại một game khác là game phá gạch (Breakout).
Breakout
Game phá gạch được phát riển bởi Atari vào năm 1976. Trong game, người chơi sẽ di chuyển một thanh đỡ cố gắng đỡ quả bóng bay lên phá vỡ đống gạch phía trên. Khi số gạch đã bị phá hết thì người chơi chiến thắng, nếu người chơi không đỡ được quả bóng làm quả bóng rơi xuống dưới thanh đỡ thì người chơi thua.
Mô tả game
Game của chúng ta có một thanh đỡ (Paddle), một quả bóng (Ball) và 30 viên gạch (Brick). Chúng ta sẽ dùng timer để chạy vòng lặp game. Game này chúng ta làm đơn giản thôi nên không cần thêm hệ thống tính điểm vào làm gì.
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 ; }; |
Đây là file header của lớp paddle. Hai hằng số INITIAL_X
và INITIAL_Y
là tọa độ khởi đầu của paddle.
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 | #include <iostream> #include "paddle.h" Paddle::Paddle() { dx = 0 ; image.load( "paddle.png" ); image = image.scaled( 70 , 10 ); 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; } |
Hướng di chuyển của Paddle là trái-phải.
1 2 3 4 5 6 7 | Paddle::Paddle() { dx = 0 ; image.load( "paddle.png" ); image = image.scaled( 70 , 10 ); rect = image.rect(); resetState(); } |
Load ảnh của paddle, bạn có thể lên google search từ khóa “paddle sprite” để tìm ảnh làm mẫu. Sau khi load ảnh lên thì phải resize về kích thước 70×10 vì ảnh gốc của chúng ta có thể lớn hơn kích thước đó. Các bạn có thể resize về kích thước khác nếu thích.
1 2 3 4 5 6 7 | void Paddle::move() { int x = rect.x() + dx; int y = rect.top(); rect.moveTo(x, y); } |
Phương thức move()
di chuyển paddle. Tốc độ di chuyển của paddle được định nghĩa bởi biến dx
.
1 2 3 4 | void Paddle::resetState() { rect.moveTo(INITIAL_X, INITIAL_Y); } |
Phương thức resetState()
sẽ đưa paddle về vị trí khởi đầu.
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; }; |
Đây là file header của lớp Brick. Biến destroyed
lưu trữ trạng thái hiện tại của brick là còn hay đã bị phá.
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 | #include <iostream> #include "brick.h" Brick::Brick( int x, int y) { image.load( "brick.png" ); image = image.scaled( 40 , 12 ); 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; } |
1 2 3 4 5 6 7 8 | Brick::Brick( int x, int y) { image.load( "brickie.png" ); image = image.scaled( 40 , 12 ); destroyed = false ; rect = image.rect(); rect.translate(x, y); } |
Trong phương thức khởi tạo, chúng ta load ảnh của brick, resize về kích thước 40×12 và thiết lập biến destroyed
là false.
1 2 3 4 | bool Brick::isDestroyed() { return destroyed; } |
Phương thức isDestroyed()
trả về trạng thái của brick.
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 ; }; |
Đây là file header của lớp Ball. Hai biến xdir
và ydir
lưu trữ hướng đi của quả bóng, hai biến này có giá trị -1 hoặc 1.
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 | #include <iostream> #include "ball.h" Ball::Ball() { xdir = 1 ; ydir = - 1 ; image.load( "ball.png" ); image = image.scaled( 12 , 12 ); rect = image.rect(); resetState(); } Ball::~Ball() { std::cout << ( "Ball deleted" ) << std::endl; } 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 ; } } void Ball::resetState() { rect.moveTo(INITIAL_X, INITIAL_Y); } void Ball::setXDir( int x) { xdir = x; } void Ball::setYDir( int y) { ydir = y; } int Ball::getXDir() { return xdir; } int Ball::getYDir() { return ydir; } QRect Ball::getRect() { return rect; } QImage & Ball::getImage() { return image; } |
1 2 | xdir = 1 ; ydir = - 1 ; |
Chúng ta khởi tạo cho quả bóng đi theo hướng đông bắc.
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 ; } } |
Phương thức autoMove()
được gọi theo vòng lặp của game. Nếu quả bóng chạm với tường trái, phải hay phía trên thì hướng của nó sẽ được thiết lập là hướng ngược lại, còn nếu chạm với bờ tường phía dưới thì game over.
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; }; |
Đây là file header của lớp Breakout, lớp này điều khiển toàn bộ game.
1 2 | void keyPressEvent(QKeyEvent *); void keyReleaseEvent(QKeyEvent *); |
Paddle sẽ được điều khiển bằng các phím mũi tên. Hai phương thức keyPressEvent()
và keyReleaseEvent()
lắng nghe sự kiện bấm phím từ người dùng.
1 2 | int x; int timerId; |
Biến x
lưu trữ tọa độ x hiện tại của paddle.
1 | static const int N_OF_BRICKS = 30 ; |
N_OF_BRICKS
là số lượng các brick.
1 | static const int DELAY = 10 ; |
Hằng số DELAY
lưu trữ tốc độ của game.
1 | static const int BOTTOM_EDGE = 400 ; |
BOTTOM_EDGE
là giới hạn phía dưới cửa sổ, nếu quả bóng chạm vào thì game over.
1 2 3 4 | bool gameOver; bool gameWon; bool gameStarted; bool paused; |
Bốn biến ở trên lưu trữ trạng thái hiện thời của game.
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 | #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 ); } } } } |
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++; } } |
Trong phương thức khởi tạo của lớp Breaout, chúng ta khởi tạo 30 viên gạch trên 5 hàng 6 cột.
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); } } |
Trong phương thức paintEvent(),
nếu trạng thái của game là đang chơi thì chúng ta vẽ paddle, ball và brick, nếu không phải thì chúng ta vẽ các đoạn chữ “Game lost”, “Victory” tùy trường hợp.
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); } |
Trong phương thức finishGame()
cũng vậy, phương thức này vẽ đoạn chữ trên màn hình và tùy vào game đã thắng hay thua mà chúng ta vẽ cho phù hợp.
1 2 3 4 5 6 7 8 9 10 | 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()); } } } |
Phương thức drawObjects()
vẽ tất cả các đối tượng có trong game.
1 2 3 4 5 6 7 8 | void Breakout::timerEvent(QTimerEvent *e) { Q_UNUSED(e); moveObjects(); checkCollision(); repaint(); } |
Phương thức timerEvent(),
là vòng lặp chính của game, cứ mỗi lần lặp, chúng ta vẽ các đối tượng, kiểm tra sự va chạm của các đối tượng và gọi phương thức repaint()
.
1 2 3 4 5 | void Breakout::moveObjects() { ball->autoMove(); paddle->move(); } |
Phương thức moveObjects()
di chuyển quả bóng và thanh đỡ.
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 ; } } |
Khi người chơi thả phím thì thanh đỡ ngừng di chuyển, chúng ta thiết lập với dx
về 0.
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); } } |
Tại phương thức keyPressEvent(),
chúng ta lắng nghe các sự kiện bấm phím. Cữ mỗi lần bấm các phím mũi tên trái hoặc phải, chúng ta set biến dx về -1 hoặc 1. Nếu bấm phím P
thì game sẽ tạm dừng, nếu bấm phím Esc
thì thoát toàn bộ game, bấm phím Space
thì game sẽ bắt đầu chơi.
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); } } |
Phương thức startGame()
reset lại vị trí paddle, vị trí ball, thiết lập trạng thái của các brick về lúc chưa bị hủy và chạy timer.
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); } } |
Phương thức pauseGame()
được dùng để dừng game hoặc cho game tiếp tục chạy. Trạng thái dừng của game được lưu trong biến pause
d.
Để dừng game thì chúng ta chỉ đơn giản là dừng vòng lặp chứ không thay đổi gì trong các phương thức di chuyển của các lớp kia.
1 2 3 4 5 6 | void Breakout::stopGame() { killTimer(timerId); gameOver = true ; gameStarted = false ; } |
Phương thức stopGame()
sẽ hủy timer và thiết lập các biến trạng thái cho phù hợp với trạng thái của game.
1 2 3 4 5 6 7 | void Breakout::checkCollision() { if (ball->getRect().bottom() > BOTTOM_EDGE) { stopGame(); } ... } |
Trong phương thức checkCollision(),
chúng ta kiểm tra sự va chạm giữa các đối tượng. Nếu quả bóng chạm tới đáy màn hình thì game kết thúc.
1 2 3 4 5 6 7 8 | for ( int i= 0 , j= 0 ; i<N_OF_BRICKS; i++) { if (bricks[i]->isDestroyed()) { j++; } if (j == N_OF_BRICKS) { victory(); } } |
Chúng ta kiểm tra xem có bao nhiêu viên gạch đã bị hủy, nếu số gạch bị hủy bằng 30 thì người chơi chiến thắng.
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(); } |
