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_WIDTH
vàB_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(); }