Qt 5 C++ – Trò chơi phá gạch

4/5 - (4 votes)

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ì.

#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_XINITIAL_Y là tọa độ khởi đầu của paddle.

#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.

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.

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.

void Paddle::resetState() {
    
  rect.moveTo(INITIAL_X, INITIAL_Y);
}

Phương thức resetState() sẽ đưa paddle về vị trí khởi đầu.

#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á.

#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;
}
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.

bool Brick::isDestroyed() {
    
  return destroyed;
}

Phương thức isDestroyed() trả về trạng thái của brick.

#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 xdirydir lưu trữ hướng đi của quả bóng, hai biến này có giá trị -1 hoặc 1.

#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;
}
xdir = 1;
ydir = -1;

Chúng ta khởi tạo cho quả bóng đi theo hướng đông bắc.

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.

#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.

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()keyReleaseEvent() lắng nghe sự kiện bấm phím từ người dùng.

int x;
int timerId;

Biến x lưu trữ tọa độ x hiện tại của paddle.

static const int N_OF_BRICKS = 30;

N_OF_BRICKS là số lượng các brick.

static const int DELAY = 10;

Hằng số DELAY lưu trữ tốc độ của game.

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.

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.

#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);
      }
    }
  }
}
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.

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.

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.

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.

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().

void Breakout::moveObjects() {

  ball->autoMove();
  paddle->move();
}

Phương thức moveObjects() di chuyển quả bóng và thanh đỡ.

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.

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.

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.

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 paused. Để 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.

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.

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.

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.

#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();
}
Untitled
4 1 vote
Article Rating
Subscribe
Thông báo cho tôi qua email khi
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Inline Feedbacks
View all comments