跳轉到

第 3 步:迷宮

沒有牆的 Pac-Man 太好走了。這一步要蓋一座迷宮,而且我們的地圖是用文字畫出來的。

第 3 步的畫面:藍色牆壁組成的迷宮,小精靈在左上角的通道裡

完整程式

main.cpp
// 第 3 步:迷宮——撞到牆就走不過去。
#include "GridPlusPlus.h"

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

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

Maze maze;

// 讓 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 MovePacman(GameEngine game, GridObject self) {
    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);
    }
}

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

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

    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

程式怎麼運作

用文字畫地圖

const char* kMap[kRows] = {
    "###############",
    "#P............#",
    ...
};

這是一個字串陣列,每個字串是地圖的一列,每個字元是一格:# 是牆,其他都是通道。kMap[y][x] 就是第 y 列、第 x 欄的那一格。.、P、G 現在還用不到,之後的步驟會派上用場。

把牆蓋起來

for (int y = 0; y < kRows; ++y) {
    for (int x = 0; x < kCols; ++x) {
        if (kMap[y][x] == '#') maze.setWall(x, y);
    }
}

兩層迴圈走過地圖的每一格,看到 # 就呼叫 maze.setWall(x, y) 在那裡蓋牆。setWallImages 會依照每面牆和隔壁牆的相連方式,自動選出直線、轉角或交叉的圖片,所以牆壁會連成漂亮的形狀。

撞牆就停下來

void Walk(GridObject who, int dx, int dy) {
    if (maze.isWall(who.x() + dx, who.y() + dy)) return;
    who.move(dx, dy);
}

走之前先算出下一格的座標,用 maze.isWall 問問看那裡是不是牆。是牆就 return,不移動。我們把這段寫成 Walk 函式,因為下一步的鬼也會用到它。

試試看

  • 改改看 kMap,畫一座你自己的迷宮。(注意:每一列都要剛好 15 個字元,最外圈最好都是牆。)
  • 把 48 改成 32 或 64,整個畫面會怎麼變?
  • 挑戰:在地圖中間挖一條「祕密通道」,讓小精靈從左邊牆壁的洞穿出去,從右邊出來。(提示:先把左右兩邊同一列的 # 改成 .,再在 Walk 裡判斷 x 超出範圍的情況。)
跑不起來,或結果和圖片不一樣?

先確認整份程式都有貼上、而且有存檔。還是不行的話,可以直接執行這一步的參考答案,看看哪裡不同:專案的 steps/ 資料夾裡有每一步的完整程式。

想知道更多?

Maze 的更多用法,例如從檔案讀取地圖,請看地圖與迷宮。

第 4 步:鬼來了