Daily Archives: 18/01/2016

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

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

Qt 5 C++ – Trò chơi rắn săn mồi

Trong phần này chúng ta sẽ viết lại một game cổ điển đó là game rắn săn mồi (Snake Game).

Snake

Game này được phát hành vào những năm 70 trên các hệ máy cầm tay, một thời gian sau được đưa lên PC. Trong game người chơi sẽ điều khiển một con rắn. Mục tiêu của người chơi là cố gắng cho con rắn ăn được càng nhiều mồi càng tốt. Mỗi lần con rắn ăn mồi, cơ thể nó sẽ dài ra. Game kết thúc khi người chơi cho rắn “ăn” phải tường hoặc cơ thể chính mình.

Mô tả game

Kích thước của mỗi đốt trên con rắn là 10px. Con rắn sẽ được điều khiển bằng các phím mũi tên. Khi game kết thúc, dòng chữ “Game Over” sẽ hiện lên giữa màn hình.

#pragma once

#include <QWidget>
#include <QKeyEvent>

class Snake : public QWidget {
    
  public:
      Snake(QWidget *parent = 0);

  protected:
      void paintEvent(QPaintEvent *);
      void timerEvent(QTimerEvent *);
      void keyPressEvent(QKeyEvent *);

  private:    
      QImage apple;
    
      static const int B_WIDTH = 300;
      static const int B_HEIGHT = 300;
      static const int DOT_SIZE = 10;
      static const int ALL_DOTS = 900;
      static const int RAND_POS = 29;
      static const int DELAY = 140;    
      
      int timerId;
      int dots;
      int apple_x;
      int apple_y;      
      
      int x[ALL_DOTS]; 
      int y[ALL_DOTS]; 
      
      int startAngle = 0;
      int spanAngle = 16 * 360;
      bool leftDirection;
      bool rightDirection;
      bool upDirection;
      bool downDirection;
      bool inGame;
      
      void loadImages();
      void initGame();
      void locateApple();
      void checkApple();
      void checkCollision();
      void move();
      void doDrawing();
      void gameOver(QPainter &);      
};
static const int B_WIDTH = 300;
static const int B_HEIGHT = 300;
static const int DOT_SIZE = 10;
static const int ALL_DOTS = 900;
static const int RAND_POS = 29;
static const int DELAY = 140;  

Ý nghĩa của các hằng số trên như sau:

  • B_WIDTHB_HEIGHT là kích thước cửa sổ chính.
  • DOT_SIZE là kích thước của mồi và mỗi đốt trên con rắn.
  • ALL_DOTS là số lượng đốt tối đa của rắn. Vì cửa sổ có kích thước 300 * 300, mỗi đốt rắn có kích thước 10 * 10 nến số lượng đốt tối đa là (300 * 300) / (10 * 10) = 900.
  • RAND_POS là hằng số để tính vị trí ngẫu nhiên của mồi.
  • DELAY là tốc độ của game.
int x[ALL_DOTS]; 
int y[ALL_DOTS]; 

Hai mảng x, y lưu trữ vị trí của toàn bộ đốt của con rắn.

#include <QPainter>
#include <QTime>
#include "snake.h"

Snake::Snake(QWidget *parent) : QWidget(parent) {

    setStyleSheet("background-color:black;");
    leftDirection = false;
    rightDirection = true;
    upDirection = false;
    downDirection = false;
    inGame = true;

    resize(B_WIDTH, B_HEIGHT);
    loadImages();
    initGame();
}

void Snake::loadImages() {   
    apple.load("apple.png");
    apple = apple.scaled(DOT_SIZE, DOT_SIZE);
}

void Snake::initGame() {

    dots = 3;

    for (int z = 0; z < dots; z++) {
        x[z] = 50 - z * 10;
        y[z] = 50;
    }

    locateApple();

    timerId = startTimer(DELAY);
}
    
void Snake::paintEvent(QPaintEvent *e) {
  
    Q_UNUSED(e);  

    doDrawing();
}    
    
void Snake::doDrawing() {
    
    QPainter qp(this);
    
    if (inGame) {

        qp.drawImage(apple_x, apple_y, apple);

        for (int z = 0; z < dots; z++) { 
             if (z == 0) { 
                 qp.setBrush(QBrush("red")); 
                 qp.drawChord(x[z], y[z], DOT_SIZE, DOT_SIZE, startAngle, spanAngle); 
             } else { 
                 qp.setBrush(QBrush("green")); 
                 qp.drawChord(x[z], y[z], DOT_SIZE, DOT_SIZE, startAngle, spanAngle); 
             } 
        } 
     } else { 
        gameOver(qp); 
     } 
} 

void Snake::gameOver(QPainter &qp) { 
     QString message = "Game over"; 
     QFont font("Courier", 15, QFont::DemiBold); 
     QFontMetrics fm(font); 

     int textWidth = fm.width(message); 
     qp.setFont(font); 
     int h = height(); 
     int w = width(); 

     qp.setPen(QPen(QBrush("white"), 1)); 
     qp.translate(QPoint(w/2, h/2)); 
     qp.drawText(-textWidth/2, 0, message); 
} 

void Snake::checkApple() { 
     if ((x[0] == apple_x) && (y[0] == apple_y)) { 
          dots++; 
          locateApple(); 
     } 
} 

void Snake::move() { 
    for (int z = dots; z > 0; z--) {
        x[z] = x[(z - 1)];
        y[z] = y[(z - 1)];
    }

    if (leftDirection) {
        x[0] -= DOT_SIZE;
    }

    if (rightDirection) {
        x[0] += DOT_SIZE;
    }

    if (upDirection) {
        y[0] -= DOT_SIZE;
    }

    if (downDirection) {
        y[0] += DOT_SIZE;
    }
}

void Snake::checkCollision() {

    for (int z = dots; z > 0; z--) {

        if ((z > 4) && (x[0] == x[z]) && (y[0] == y[z])) {
            inGame = false;
        }
    }

    if (y[0] >= B_HEIGHT) {
        inGame = false;
    }

    if (y[0] < 0) { 
        inGame = false; 
    } 

    if (x[0] >= B_WIDTH) {
        inGame = false;
    }

    if (x[0] < 0) { 
        inGame = false; 
    } 

    if(!inGame) { 
        killTimer(timerId); 
    } 
} 

void Snake::locateApple() { 
    QTime time = QTime::currentTime(); 
    qsrand((uint) time.msec()); 
    int r = qrand() % RAND_POS; 
    apple_x = (r * DOT_SIZE); 
    r = qrand() % RAND_POS; 
    apple_y = (r * DOT_SIZE); 
} 

void Snake::timerEvent(QTimerEvent *e) { 
    Q_UNUSED(e); 
    if (inGame) { 
        checkApple(); 
        checkCollision(); 
        move(); 
    } 

    repaint(); 
} 

void Snake::keyPressEvent(QKeyEvent *e) { 
    int key = e->key();
    
    if ((key == Qt::Key_Left) && (!rightDirection)) {
        leftDirection = true;
        upDirection = false;
        downDirection = false;
    }

    if ((key == Qt::Key_Right) && (!leftDirection)) {
        rightDirection = true;
        upDirection = false;
        downDirection = false;
    }

    if ((key == Qt::Key_Up) && (!downDirection)) {
        upDirection = true;
        rightDirection = false;
        leftDirection = false;
    }

    if ((key == Qt::Key_Down) && (!upDirection)) {
        downDirection = true;
        rightDirection = false;
        leftDirection = false;
    }    
    
    QWidget::keyPressEvent(e);    
}
void Snake::loadImages() {
    
    apple.load("apple.png");
    apple = apple.scaled(DOT_SIZE, DOT_SIZE);
}

Phương thức loadImages() load ảnh quả táo dùng làm mồi cho con rắn. Vì ảnh chúng ta dùng có thể có kích thước khác 10*10 nên ta phải resize kích thước ảnh lại bằng phương thức scaled().

void Snake::initGame() {

    dots = 3;

    for (int z = 0; z < dots; z++) {
        x[z] = 50 - z * 10;
        y[z] = 50;
    }

    locateApple();

    timerId = startTimer(DELAY);
}

Trong phương thức initGame() chúng ta cho khởi tạo game, bao gồm khởi tạo số lượng đốt của rắn, khởi tạo vị trí ngẫu nhiên của mồi và chạy timer.

void Snake::checkApple() {

    if ((x[0] == apple_x) && (y[0] == apple_y)) {

        dots++;
        locateApple();
    }
}

Bên trong phương thức checkApple() chúng ta kiểm tra xem nếu vị trí đầu rắn có trùng khớp với vị trí của mồi thì chúng ta tăng số lượng đốt lên và khởi tạo mồi mới.

void Snake::move() {

    for (int z = dots; z > 0; z--) {
        x[z] = x[(z - 1)];
        y[z] = y[(z - 1)];
    }

    if (leftDirection) {
        x[0] -= DOT_SIZE;
    }

    if (rightDirection) {
        x[0] += DOT_SIZE;
    }

    if (upDirection) {
        y[0] -= DOT_SIZE;
    }

    if (downDirection) {
        y[0] += DOT_SIZE;
    }
}

Phương thức move() cập nhật vị trí mới của con rắn, các đốt của con rắn sẽ có vị trí mới là vị trí của đốt phía trước con rắn, đối với đốt đầu tiên thì chúng ta dựa vào hướng di chuyển hiện tại để tính vị trí mới.

Phương thức checkCollision() kiểm tra xem nếu con rắn có đụng đầu vào tường hay cắn chính mình hay không.

for (int z = dots; z > 0; z--) {

    if ((z > 4) && (x[0] == x[z]) && (y[0] == y[z])) {
        inGame = false;
    }
}

Nếu con rắn cắn chính mình thì game over.

if (y[0] >= B_HEIGHT) {
    inGame = false;
}

Nếu con rắn chạm tường thì cũng game over.

void Snake::timerEvent(QTimerEvent *e) {
    
    Q_UNUSED(e);  
    
    if (inGame) {

        checkApple();
        checkCollision();
        move();
    }

    repaint();
}  

Phương thức timerEvent() là vòng lặp của game, cứ mỗi lần lặp chúng ta kiểm tra con rắn có ăn mồi, đụng tường hay cắn chính mình hay không và cập nhật vị trí con rắn.

if ((key == Qt::Key_Left) && (!rightDirection)) {
    leftDirection = true;
    upDirection = false;
    downDirection = false;
}

Nếu bấm phím mũi tên trái và hướng đi của con rắn không phải hướng ngược lại – tức bên phải thì chúng ta cập nhật lại hướng đi mới của con rắn.

#include <QApplication>
#include "snake.h"

int main(int argc, char *argv[]) {
    
  QApplication app(argc, argv);
  
  Snake window;

  window.setWindowTitle("Snake");
  window.show();

  return app.exec();
}
Untitled