@@ -0,0 +1,241 @@
|
||||
// =============================================================================
|
||||
// examples/pacman —— Grid++ 範例:Pac-Man 小遊戲
|
||||
// 逐步拆解見文件 docs/tutorial/pacman.md。
|
||||
//
|
||||
// 流程:開始畫面 →(按 Start)遊戲中 → 獲勝 / 失敗 →(按 Restart)重來一局。
|
||||
// 玩法:方向鍵移動(持續朝目前方向走,按方向鍵且該方向不是牆才轉向),
|
||||
// 吃光所有豆子獲勝,被任何鬼魂抓到失敗。
|
||||
//
|
||||
// map.txt 格式:第一行「列數 欄數」,之後 rows*cols 個整數
|
||||
// (1=牆 0=通道豆子 2=玩家 3=鬼魂)。
|
||||
//
|
||||
// 編譯(在本資料夾內,raylib 已安裝):
|
||||
// g++ main.cpp -I../.. -o game -lraylib -lopengl32 -lgdi32 -lwinmm
|
||||
// 執行前確保本資料夾有 map.txt 與 assets.db
|
||||
// (assets.db 用根目錄的 create_assets.py 產生:`python ../../create_assets.py`)。
|
||||
// =============================================================================
|
||||
#include "GridMaze.h" // 用 -I../.. 指到專案根;會自動帶進核心 GridPlusPlus.h
|
||||
#include "GridUI.h" // Label / Button 等 UI 元件
|
||||
#include <fstream> // 讀地圖檔
|
||||
#include <string>
|
||||
|
||||
using namespace std;
|
||||
|
||||
// 本範例除了 string 外不使用 STL 容器:地圖邊讀邊處理,物件直接 spawn。
|
||||
|
||||
// 遊戲階段:0=開始畫面, 1=遊戲中, 2=獲勝, 3=失敗
|
||||
int g_state = 0;
|
||||
int g_pelletsLeft = 0; // 還剩幾顆豆子
|
||||
bool g_paused = false; // 是否暫停(由暫停按鈕切換)
|
||||
GridMaze* g_maze = nullptr; // 讓角色查詢牆壁
|
||||
GridObject* g_player = nullptr; // 讓鬼魂知道玩家在哪
|
||||
GridEngine* g_game = nullptr; // 讓「重新開始」按鈕能重建世界
|
||||
|
||||
// 四個方向,對應 setDirection 的 0~3:右、上、左、下
|
||||
static const int DX[4] = { 1, 0, -1, 0 };
|
||||
static const int DY[4] = { 0, -1, 0, 1 };
|
||||
|
||||
// 曼哈頓距離(自己寫,避免引入 <cstdlib> 的 abs)
|
||||
static int manhattan(int ax, int ay, int bx, int by) {
|
||||
int dx = ax - bx, dy = ay - by;
|
||||
if (dx < 0) dx = -dx;
|
||||
if (dy < 0) dy = -dy;
|
||||
return dx + dy;
|
||||
}
|
||||
|
||||
// 豆子:被小精靈吃到就消失
|
||||
class Pellet : public GridObject {
|
||||
public:
|
||||
Pellet(int x, int y) : GridObject("pellet", x, y) { setTag("pellet"); }
|
||||
|
||||
void onCollide(GridObject* other) override {
|
||||
if (!eaten && other->getTag() == "pacman") {
|
||||
eaten = true;
|
||||
if (--g_pelletsLeft <= 0) g_state = 2; // 吃完 → 獲勝
|
||||
}
|
||||
}
|
||||
void render(GridEngine* engine) override {
|
||||
if (!eaten) GridObject::render(engine);
|
||||
}
|
||||
private:
|
||||
bool eaten = false;
|
||||
};
|
||||
|
||||
// 鬼魂:貪心追逐(挑離玩家最近、且不回頭的方向)或隨機。各自有不同顏色(tint)。
|
||||
class Ghost : public GridObject {
|
||||
public:
|
||||
Ghost(int x, int y, Color color, bool isRandom)
|
||||
: GridObject("ghost", x, y), random(isRandom) {
|
||||
setTag("ghost");
|
||||
setTint(color); // 同一張白色素材染成不同顏色
|
||||
}
|
||||
|
||||
void onUpdate() override {
|
||||
if (g_state != 1 || g_paused || !g_player) return;
|
||||
if (++timer < 12) return; // 移動速度(比玩家稍慢)
|
||||
timer = 0;
|
||||
|
||||
int options[4], nopt = 0; // 非牆、且不回頭
|
||||
int any[4], nany = 0; // 非牆(死路時才用,允許回頭)
|
||||
for (int d = 0; d < 4; d++) {
|
||||
if (g_maze->isWall(getX() + DX[d], getY() + DY[d])) continue;
|
||||
any[nany++] = d;
|
||||
if (d == (dir ^ 2)) continue; // d 與目前方向相反 → 回頭,先跳過
|
||||
options[nopt++] = d;
|
||||
}
|
||||
int* pool = nopt ? options : any;
|
||||
int n = nopt ? nopt : nany;
|
||||
if (n == 0) return;
|
||||
|
||||
int pick = pool[0];
|
||||
if (random) {
|
||||
pick = pool[GetRandomValue(0, n - 1)]; // 隨機鬼
|
||||
} else { // 貪心鬼:挑離玩家曼哈頓距離最小的
|
||||
int best = 1 << 30;
|
||||
for (int k = 0; k < n; k++) {
|
||||
int d = pool[k];
|
||||
int dist = manhattan(getX() + DX[d], getY() + DY[d],
|
||||
g_player->getX(), g_player->getY());
|
||||
if (dist < best) { best = dist; pick = d; }
|
||||
}
|
||||
}
|
||||
dir = pick;
|
||||
move(DX[dir], DY[dir]);
|
||||
}
|
||||
private:
|
||||
bool random;
|
||||
int timer = 0;
|
||||
int dir = 0;
|
||||
};
|
||||
|
||||
// 小精靈(玩家):持續朝目前方向走;按方向鍵且該方向不是牆才轉向。
|
||||
class Pacman : public GridObject {
|
||||
public:
|
||||
Pacman(int x, int y) : GridObject("pacman", x, y) { setTag("pacman"); }
|
||||
|
||||
void onUpdate() override {
|
||||
if (g_state != 1 || g_paused) return;
|
||||
if (IsKeyDown(KEY_RIGHT)) want = 0; // 每幀記下想要的方向(緩衝)
|
||||
if (IsKeyDown(KEY_UP)) want = 1;
|
||||
if (IsKeyDown(KEY_LEFT)) want = 2;
|
||||
if (IsKeyDown(KEY_DOWN)) want = 3;
|
||||
|
||||
if (++timer < 8) return; // 移動速度
|
||||
timer = 0;
|
||||
|
||||
if (want >= 0 && !g_maze->isWall(getX() + DX[want], getY() + DY[want]))
|
||||
dir = want; // 想要的方向合法 → 轉過去
|
||||
if (dir >= 0 && !g_maze->isWall(getX() + DX[dir], getY() + DY[dir])) {
|
||||
move(DX[dir], DY[dir]);
|
||||
setDirection(dir); // 旋轉素材朝向移動方向
|
||||
}
|
||||
}
|
||||
void onCollide(GridObject* other) override {
|
||||
if (other->getTag() == "ghost") g_state = 3; // 被鬼抓到 → 失敗
|
||||
}
|
||||
private:
|
||||
int timer = 0;
|
||||
int dir = -1; // -1 = 還沒開始移動
|
||||
int want = -1;
|
||||
};
|
||||
|
||||
// 依 map.txt 佈置一局:清掉上一局 → 重建迷宮、豆子、角色。開始與重新開始都呼叫它。
|
||||
void buildLevel(GridEngine& game) {
|
||||
game.clearObjects();
|
||||
g_pelletsLeft = 0;
|
||||
g_paused = false;
|
||||
|
||||
ifstream f("map.txt");
|
||||
int rows, cols;
|
||||
f >> rows >> cols;
|
||||
|
||||
GridMaze* maze = new GridMaze(cols, rows);
|
||||
// 自動拼接:給 6 種基本牆形狀,其餘方向引擎會旋轉素材湊出來。
|
||||
maze->setWallTiles("wall_iso", "wall_end", "wall_straight",
|
||||
"wall_corner", "wall_tee", "wall_cross");
|
||||
game.spawn(maze); // 先放迷宮(畫最底層)
|
||||
g_maze = maze;
|
||||
|
||||
Color colors[4] = { RED, PINK, SKYBLUE, ORANGE }; // 四隻鬼的顏色
|
||||
int gi = 0;
|
||||
Pacman* player = nullptr;
|
||||
for (int y = 0; y < rows; y++)
|
||||
for (int x = 0; x < cols; x++) {
|
||||
int t;
|
||||
f >> t;
|
||||
if (t == 1) maze->setWall(x, y, true);
|
||||
else if (t == 0) { game.spawn(new Pellet(x, y)); g_pelletsLeft++; }
|
||||
else if (t == 2) player = new Pacman(x, y);
|
||||
else if (t == 3) { game.spawn(new Ghost(x, y, colors[gi % 4], gi == 3)); gi++; }
|
||||
}
|
||||
if (player) { g_player = player; game.spawn(player); } // 玩家最後生成(畫最上層)
|
||||
}
|
||||
|
||||
// 分數與各畫面的訊息(UI 覆蓋層,跳脫網格、用像素座標)
|
||||
class ScoreUI : public UIElement {
|
||||
public:
|
||||
void draw() override {
|
||||
if (g_state == 1) { // 遊戲中:左上角分數
|
||||
DrawText(TextFormat("Pellets: %d", g_pelletsLeft), 8, 8, 20, YELLOW);
|
||||
if (g_paused) bigText("PAUSED", ORANGE);
|
||||
return;
|
||||
}
|
||||
// 非遊戲中:壓暗背景,畫大字(按鈕由各自的類別畫在這之上)
|
||||
DrawRectangle(0, 0, GetScreenWidth(), GetScreenHeight(), Fade(BLACK, 0.6f));
|
||||
if (g_state == 0) bigText("PAC-MAN", YELLOW);
|
||||
else if (g_state == 2) bigText("YOU WIN!", GREEN);
|
||||
else if (g_state == 3) bigText("GAME OVER", RED);
|
||||
}
|
||||
private:
|
||||
static void bigText(const char* msg, Color col) {
|
||||
int fs = 40, tw = MeasureText(msg, fs);
|
||||
DrawText(msg, GetScreenWidth() / 2 - tw / 2, GetScreenHeight() / 2 - 70, fs, col);
|
||||
}
|
||||
};
|
||||
|
||||
// 各按鈕只在「對的階段」顯示與作用:覆寫 onClick(行為)與 onUpdate/draw(階段判斷)。
|
||||
class StartButton : public Button {
|
||||
public:
|
||||
StartButton(int x, int y, int w, int h) : Button("Start", x, y, w, h) {}
|
||||
void onClick() override { g_state = 1; } // 開始遊戲
|
||||
void onUpdate() override { if (g_state == 0) Button::onUpdate(); }
|
||||
void draw() override { if (g_state == 0) Button::draw(); }
|
||||
};
|
||||
|
||||
class RestartButton : public Button {
|
||||
public:
|
||||
RestartButton(int x, int y, int w, int h) : Button("Restart", x, y, w, h) {}
|
||||
void onClick() override { buildLevel(*g_game); g_state = 1; } // 重來一局
|
||||
void onUpdate() override { if (g_state == 2 || g_state == 3) Button::onUpdate(); }
|
||||
void draw() override { if (g_state == 2 || g_state == 3) Button::draw(); }
|
||||
};
|
||||
|
||||
class PauseButton : public Button {
|
||||
public:
|
||||
PauseButton(int x, int y, int w, int h) : Button("Pause", x, y, w, h) {}
|
||||
void onClick() override { g_paused = !g_paused; }
|
||||
void onUpdate() override { if (g_state == 1) Button::onUpdate(); }
|
||||
void draw() override { if (g_state == 1) Button::draw(); }
|
||||
};
|
||||
|
||||
int main() {
|
||||
int rows, cols;
|
||||
{ ifstream f("map.txt"); f >> rows >> cols; } // 先讀尺寸決定視窗大小
|
||||
|
||||
GridEngine game(cols, rows, 32);
|
||||
g_game = &game;
|
||||
game.loadAssets("assets.db");
|
||||
|
||||
buildLevel(game); // 先建好一局
|
||||
g_state = 0; // 停在開始畫面
|
||||
|
||||
// UI 覆蓋層(ScoreUI 先加,按鈕畫在它之上)
|
||||
int bw = 120, bh = 40, bx = cols * 32 / 2 - bw / 2, by = rows * 32 / 2;
|
||||
game.addUI(new ScoreUI());
|
||||
game.addUI(new StartButton(bx, by, bw, bh));
|
||||
game.addUI(new RestartButton(bx, by, bw, bh));
|
||||
game.addUI(new PauseButton(cols * 32 - 88, 6, 82, 24));
|
||||
|
||||
game.run();
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user