跳轉到

第 4 步:鬼來了

找個朋友一起玩!這一步加入一隻紅色的鬼,由第二位玩家用 W、A、S、D 控制。鬼抓到小精靈,遊戲就結束。

第 4 步的畫面:小精靈在左上角,紅色的鬼在右下角

玩家 角色 按鍵
玩家 1 小精靈 方向鍵
玩家 2 鬼 W 上、A 左、S 下、D 右

完整程式

main.cpp
// 第 4 步:鬼來了——雙人對戰,被抓到就輸。
#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;
Overlay message;
bool game_over = false;

// 讓 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();
}

// 小精靈和別的東西撞在同一格時,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);
    }
}

// 第二位玩家用 W、A、S、D 控制鬼。
void MoveGhost(GameEngine game, GridObject self) {
    if (game_over) return;

    if (game.keyPressed(KEY_D)) Walk(self, 1, 0);
    if (game.keyPressed(KEY_W)) Walk(self, 0, -1);
    if (game.keyPressed(KEY_A)) Walk(self, -1, 0);
    if (game.keyPressed(KEY_S)) 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, PacmanHit);
    pacman.setPosition(1, 1);

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

    message = game.addTextOverlay("GAME OVER", 195, 230, 60, RED);
    message.hide();

    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

被抓到時會像這樣:

GAME OVER:紅色的鬼和小精靈在同一格,畫面中央出現 GAME OVER

程式怎麼運作

碰撞:撞在同一格時通知你

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

addObject 的第 4 個參數是碰撞函式。每一幀所有東西都移動完之後,Grid++ 會檢查有沒有兩個物件站在同一格;有的話,就呼叫它們的碰撞函式。

void PacmanHit(GameEngine, GridObject, GridObject other) {
    if (other.image() == "ghost") GameOver();
}

other 是撞到小精靈的那個物件。我們用 other.image() 看它的圖片名稱,是 "ghost" 才算被抓到。第 6 步會出現豆子,到時候小精靈也會撞到豆子,所以先確認撞到的是誰很重要。

遊戲結束

bool game_over = false;

game_over 記錄遊戲是否已經結束。GameOver() 會把它設成 true,並顯示原本藏起來(hide())的 GAME OVER 文字。兩個移動函式一開頭都會檢查它:遊戲結束了,就誰都不能再動。

同一個 Walk,兩個角色

鬼的移動和小精靈幾乎一樣,只是換了按鍵。因為上一步把「撞牆就停」寫成了 Walk 函式,鬼可以直接拿來用。這就是把程式寫成函式的好處。

試試看

  • 讓鬼走得比小精靈慢:鬼每按兩次才走一格。(提示:用一個變數數按了幾次。)
  • 被抓到後,按 R 重新開始。(提示:把兩隻角色放回起點,把 game_over 設回 false,再用 message.hide() 藏起文字。)
  • 再加一隻鬼,用 I、J、K、L 控制,變成三人對戰!
跑不起來,或結果和圖片不一樣?

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

想知道更多?

碰撞的完整規則,例如哪些物件不會被檢查,請看同格互動。

第 5 步:鬼會自己追