跳轉到

第 6 步:吃豆子

最後一塊拼圖!地圖上的每個 . 都變成一顆豆子。吃一顆加 10 分,全部吃光就贏了;被鬼抓到還是會輸。

第 6 步的畫面:迷宮裡擺滿豆子,小精靈吃掉了幾顆,左上角顯示 Score: 40

完整程式

main.cpp
// 第 6 步:吃豆子——吃光所有豆子就贏了!
#include <cstdlib>
#include <string>

#include "GridPlusPlus.h"

using gridpp::GameEngine;
using gridpp::GridObject;
using gridpp::Maze;
using gridpp::Overlay;

const int kCols = 15;
const int kRows = 11;
const char* kMap[kRows] = {
    "###############",
    "#P............#",
    "#.#.###.###.#.#",
    "#.............#",
    "#.#.#.#.#.#.#.#",
    "#.............#",
    "#.#.#.#.#.#.#.#",
    "#.............#",
    "#.#.###.###.#.#",
    "#............G#",
    "###############",
};  // '#' 是牆,'P' 是小精靈的起點,'G' 是鬼的起點

Maze maze;
GridObject pacman;
Overlay message;
bool game_over = false;
Overlay score_label;
int score = 0;
int pellets_left = 0;

// 讓 who 往 (dx, dy) 走一格;前面是牆就不動。
void Walk(GridObject who, int dx, int dy) {
    if (maze.isWall(who.x() + dx, who.y() + dy)) return;
    who.move(dx, dy);
}

void GameOver() {
    game_over = true;
    message.setText("GAME OVER");
    message.show();
}

void Win() {
    game_over = true;
    message.setText("YOU WIN!");
    message.setColor(YELLOW);
    message.show();
}

// 豆子和別的東西撞在同一格時,Engine 會呼叫這個函式。
void EatPellet(GameEngine, GridObject self, GridObject other) {
    if (other.image() != "pacman") return;  // 鬼經過不算,只有小精靈會吃豆子

    self.remove();
    score += 10;
    score_label.setText("Score: " + std::to_string(score));
    pellets_left--;
    if (pellets_left == 0) Win();
}

// 小精靈和別的東西撞在同一格時,Engine 會呼叫這個函式。
void PacmanHit(GameEngine, GridObject, GridObject other) {
    if (other.image() == "ghost") GameOver();
}

void MovePacman(GameEngine game, GridObject self) {
    if (game_over) return;

    if (game.keyPressed(KEY_RIGHT)) {
        self.setDirection(0);
        Walk(self, 1, 0);
    }
    if (game.keyPressed(KEY_UP)) {
        self.setDirection(1);
        Walk(self, 0, -1);
    }
    if (game.keyPressed(KEY_LEFT)) {
        self.setDirection(2);
        Walk(self, -1, 0);
    }
    if (game.keyPressed(KEY_DOWN)) {
        self.setDirection(3);
        Walk(self, 0, 1);
    }
}

const int kDX[4] = {1, 0, -1, 0};  // 方向 0 右、1 上、2 左、3 下
const int kDY[4] = {0, -1, 0, 1};
int ghost_direction = 2;
double ghost_next_move = 0;

// 鬼是不是和小精靈在同一格?
bool Caught(GridObject ghost) { return ghost.x() == pacman.x() && ghost.y() == pacman.y(); }

void ChasePacman(GameEngine game, GridObject self) {
    if (game_over) return;
    if (game.time() < ghost_next_move) return;  // 還沒輪到鬼走
    ghost_next_move = game.time() + 0.3;

    // 走之前先看一眼:小精靈是不是自己撞上來了?
    if (Caught(self)) {
        GameOver();
        return;
    }

    // 看看四個方向,選一條離小精靈最近、而且不用回頭的路。
    int back = (ghost_direction + 2) % 4;
    int best = back;  // 四面都走不通(死路)的時候,只好回頭
    int best_distance = 9999;
    for (int d = 0; d < 4; ++d) {
        if (d == back) continue;
        int x = self.x() + kDX[d];
        int y = self.y() + kDY[d];
        if (maze.isWall(x, y)) continue;
        int distance = std::abs(x - pacman.x()) + std::abs(y - pacman.y());
        if (distance < best_distance) {
            best = d;
            best_distance = distance;
        }
    }
    ghost_direction = best;
    self.move(kDX[best], kDY[best]);

    // 走完再看一眼:是不是撲到小精靈身上了?
    if (Caught(self)) GameOver();
}

int main() {
    GameEngine game(kCols, kRows, 48);
    game.loadAssets("pacman.db");
    game.setBackgroundColor(BLACK);

    maze = game.addMaze(kCols, kRows);
    maze.setWallImages("wall_iso", "wall_end", "wall_straight", "wall_corner", "wall_tee", "wall_cross");
    for (int y = 0; y < kRows; ++y) {
        for (int x = 0; x < kCols; ++x) {
            if (kMap[y][x] == '#') {
                maze.setWall(x, y);
            } else if (kMap[y][x] == '.') {
                GridObject pellet = game.addObject("pellet", nullptr, nullptr, EatPellet);
                pellet.setPosition(x, y);
                pellets_left++;
            }
        }
    }

    pacman = game.addObject("pacman", nullptr, MovePacman, PacmanHit);
    pacman.setPosition(1, 1);

    GridObject ghost = game.addObject("ghost", nullptr, ChasePacman);
    ghost.setPosition(13, 9);
    ghost.setColor(RED);

    message = game.addTextOverlay("GAME OVER", 195, 230, 60, RED);
    message.hide();
    score_label = game.addTextOverlay("Score: 0", 10, 10, 24, WHITE);

    game.run();
    return 0;
}

在專案資料夾的終端機編譯並執行(按 Up 鍵可以叫出上次的指令):

g++ -std=c++17 main.cpp -o game -lraylib -lGL -lm -lpthread -ldl -lrt -lX11 && ./game
g++ -std=c++17 main.cpp -o game $(pkg-config --cflags --libs raylib) && ./game

你能拿到幾分?吃得完嗎?

程式怎麼運作

地圖上的點變成豆子

} else if (kMap[y][x] == '.') {
    GridObject pellet = game.addObject("pellet", nullptr, nullptr, EatPellet);
    pellet.setPosition(x, y);
    pellets_left++;
}

第 3 步就畫在地圖上的 .,現在終於派上用場了。每遇到一個 . 就放一顆豆子,同時用 pellets_left 數總共有幾顆。小精靈的起點是 P,所以那一格沒有豆子。

吃掉豆子

豆子也有自己的碰撞函式 EatPellet。小精靈走到豆子上時:

  1. 先確認撞到豆子的是小精靈。鬼經過的話不算數。
  2. self.remove() 把這顆豆子從遊戲中拿掉。
  3. 加分,並更新左上角的分數。
  4. 剩下的豆子少一顆;變成 0 就呼叫 Win()。

Win() 和 GameOver() 幾乎一樣,只是顯示 YOU WIN!。兩者都把 game_over 設成 true,讓所有角色停下來。

🎉 完成了!

你剛剛從零做出了一個完整的 Pac-Man。回頭看看,你學會了:

  • 每一幀執行的 Update 函式
  • 讀取鍵盤,用 game.time() 計算時間
  • 用陣列畫地圖、判斷牆壁
  • 碰撞函式
  • 讓鬼自己做決定
  • 分數與勝負

別忘了上傳到 Gitea,把你的成果存起來!

試試看

  • 在地圖上加入「大力丸」(例如用 o 表示):吃到後 5 秒內換小精靈追鬼,抓到鬼可以加 200 分。
  • 被抓到不會馬上輸:給小精靈 3 條命,被抓到就回到起點。
  • 吃完豆子後進入第二關:換一張新地圖,鬼也變得更快。
  • 畫一張完全屬於你的地圖,讓同學來挑戰!

接下來

  • 做自己的遊戲:Gitea 上的 GridPlusPlus-Template 是一個空白的起點。用取得你的專案教過的方法 fork 它,把網址裡的 GridPlusPlus-Pacman 換成 GridPlusPlus-Template,就能開始做一個全新的遊戲。
  • 深入了解 Grid++:後面的章節會更完整地介紹 Grid++ 的設計,還有另一個小遊戲「打地鼠」,以及有開始畫面、暫停和四隻鬼的完整版 Pac-Man。想知道某個功能怎麼用的時候,再回來翻就可以了。

深入了解 Grid++