找洞鑽
\\\景資成發///
一仁徐以潔 一平廖子綺 一誠吳怡宣
分工表
遊戲發想:徐以潔 廖子綺 吳怡宣
程式設計:徐以潔
簡報:廖子綺
報告:徐以潔 廖子綺 吳怡宣
遊戲介紹
遊戲試玩
完整程式碼
程式碼(完整版):
#include <iostream>
#include <cstdio> // For puts
#include <conio.h> // For _kbhit and _getch
#include <windows.h> // For Sleep and console functions (gotoxy, SetConsoleCursorPosition)
#include <ctime> // For time(nullptr) in srand
#include <cstdlib> // For srand, rand
#include <vector> // Using vector<string> for easier map manipulation
#include <string> // Required for std::string
#include <thread> // For std::this_thread::sleep_for
#include <chrono> // For std::chrono::milliseconds
#include <io.h> // For _setmode
#include <fcntl.h> // For _O_U8TEXT
using namespace std;
// 使用 vector<string> 比 char[][] 更方便且安全地操作地圖。
// 定義地圖尺寸
const int MAP_H = 8; // 地圖高度,從 0 到 7
const int MAP_W = 22; // 地圖寬度,從 0 到 21
// 初始地圖設定 - 注意:這裡將玩家 'o' 移除,會在 resetGame 中放置
vector<wstring> play_map = {
L"######################",
L"# #",
L"# #",
L"# #",
L"# #",
L"# #",
L"# #", // 原本的玩家位置現在也初始化為空白
L"######################" // 最後一行保持為牆壁
};
struct Player {// 玩家
int x;
int y;
} player;
bool game_over = false;// 控制遊戲狀態
int final_score = 0; // 用於儲存最終分數
void welcome()
{
wcout << L"\n\n\t\t\t找 洞 鑽";
wcout << L"\n\n\t\t 請按任意鍵開始";
wcout << L"\n\n\t\t 按F可結束遊戲";
getch();//得到任意鍵
system("cls");//清空螢幕
SetConsoleTitleW(L"找洞鑽");//設置窗口標題
}
void gotoxy(int x, int y) {// 設定游標位置,讓畫面更新更流暢
COORD coord;
coord.X = x;
coord.Y = y;
SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), coord);// 將游標移動到指定的位置
}
void drawMap() {
gotoxy(0, 0); // 將游標移動到左上角,以便重新繪製整個地圖
for (int i = 0; i < MAP_H; ++i) {
wcout << play_map[i] << endl;
}
}
void reset() {// 重置遊戲地圖和玩家位置
play_map = {
L"######################",
L"# #",
L"# #",
L"# #",
L"# #",
L"# #",
L"# #",
L"######################"
};
player.x = 11; // 玩家 X 座標重置到中心
player.y = 6; // 玩家總是在y=6上
play_map[player.y][player.x] = L'o'; // 只在這裡放置玩家 'o'
game_over = false; // 重置遊戲時,將遊戲結束狀態設為 false
final_score = 0; // 重置分數
}
void xxxyyy() {// 生成新的障礙物
for (int i = 1; i < MAP_W - 1; ++i) {// 先都設定為障礙物 'x'
play_map[1][i] = L'x';
}
int hole_w = rand() % 2 + 3; // 隨機生成 3 或 4 格寬的洞,因為%2
// 計算洞的起始位置範圍
int max_hole_start_x = (MAP_W - 1) - hole_w;// 確保出現在遊戲範圍裡
int hole_start_x = rand() % max_hole_start_x + 1;// +1是因為從1開始
for (int i = 0; i < hole_w; ++i) {// 挖出第一個洞
play_map[1][hole_start_x + i] = L' ';
}
if (rand() % 10 < 3) { // 30% 機率生成另外的洞
int an_hole_w = rand() % 2 + 1; // 1 或 2 格寬
int an_hole_start_x;
bool v_found = false;// 還沒找到適合的地方
// 嘗試找一個不重疊的位置
for(int attempt = 0; attempt < 10; ++attempt) { // 嘗試幾次
an_hole_start_x = rand() % (MAP_W - 1 - an_hole_w) + 1;
bool overlap = false; // 沒有重疊到
for(int i = 0; i < an_hole_w; ++i) {
if (play_map[1][an_hole_start_x + i] == ' ') { // 如果新洞的位置已經是空格,說明重疊了
overlap = true; //有重疊
break;
}
}
if (!overlap) { // 沒有重疊
v_found = true; // 找到地方了
break;
}
}
if (v_found) {
for (int i = 0; i < an_hole_w; ++i) {
play_map[1][an_hole_start_x + i] = L' ';
}
}
}
}
// 移動所有障礙物向下,並計算分數
bool moveDown(int& current_score) { // 傳入分數引用(&=傳入分數本身不是分身)
bool ya = false; // 沒有撞到玩家
bool scored_this_frame = false; // 標記這一幀是否有分數增加
// 從底部往上遍歷所有可移動的行(不包括邊界)
for (int r = MAP_H - 2; r >= 1; --r) { // 從玩家上方一行開始往上檢查(8-2=6=r)
for (int c = 1; c < MAP_W - 1; ++c) { // 遍歷所有可移動的列
if (play_map[r][c] == L'x') { // 如果當前位置有障礙物
if (r + 1 == player.y && c == player.x) {// 檢查下方是否為玩家
ya = true; // 撞到了
}
// 將障礙物移動到下一行,只有當下一行不是最底部的牆壁時才移動
if (r + 1 < MAP_H - 1) { // 確保障礙物不會覆蓋到最底下那行
play_map[r + 1][c] = L'x';
// 如果障礙物從玩家上方一行成功移動到最後一行,並且沒有撞到 (沒有 ya),則加分
if (r == player.y - 1 && !ya) {
if (!scored_this_frame) { // 確保次只加一次分數
current_score += 1; // 增加分數
scored_this_frame = true;
}
}
}
play_map[r][c] = L' ';// 清除原位置
}
}
}
return ya; // 返回是否發生碰撞
}
void movement(char move_letter) {// 處理玩家移動
play_map[player.y][player.x] = L' '; // 先將玩家目前位置清空
switch (move_letter) {
case 'a': // 向左移動
// 檢查是否超出左邊界以及目標位置是否為空(非牆壁且非障礙物)
if (player.x > 1 && play_map[player.y][player.x - 1] == L' ') {
player.x--;
}
break;
case 'd': // 向右移動
// 檢查是否超出右邊界以及目標位置是否為空(非牆壁且非障礙物)
if (player.x < MAP_W - 2 && play_map[player.y][player.x + 1] == L' ') {
player.x++;
}
break;
case 'f': // 重置遊戲
// 這裡不再直接調用 resetGame,因為還有遊戲結束和或重置的選擇
// 如果在遊戲結束後按 'f' 應該觸發重啟
if (game_over) {
// 在 main 迴圈外處理重置
}
break;
}
play_map[player.y][player.x] = L'o'; // 在新位置放置玩家
}
int main() {
// START: 確保控制台能正確顯示 UTF-8 中文
// 設置標準輸出流模式為 UTF-8 文本
_setmode(_fileno(stdout), _O_U8TEXT);
// 設置標準輸入流模式為 UTF-8 文本 (如果需要處理中文輸入的話)
_setmode(_fileno(stdin), _O_U8TEXT);
// 設置控制台輸出代碼頁為 UTF-8 (65001)
SetConsoleOutputCP(CP_UTF8);
// 設置控制台輸入代碼頁為 UTF-8 (65001) (如果需要處理中文輸入的話)
SetConsoleCP(CP_UTF8);
// END: 確保控制台能正確顯示 UTF-8 中文
welcome();
srand(static_cast<unsigned int>(time(nullptr))); // 利用時間設定亂數種子
bool play_again = true;
while (play_again) { // 外部迴圈控制遊戲是否重啟
reset(); // 遊戲開始前先重置地圖和玩家位置
final_score = 0; // 每次重置遊戲時,分數也要重置
long long last_spawn_time = GetTickCount(); // 記錄上次生成障礙物的時間
long long last_fall_time = GetTickCount(); // 記錄上次障礙物掉落的時間
// 每 1800 毫秒 (1.8秒) 生成一次障礙物
const int SPAWN_T= 1800;
// 每 600 毫秒 (0.6秒) 障礙物掉落一次
const int FALL_T = 600;
drawMap(); // 初始繪製地圖
while (!game_over) { // 當 game_over 為 true 時跳出迴圈
long long current_time = GetTickCount(); // 取得當前時間
if (_kbhit()) { // 檢查是否有按鍵被按下
char key = _getch(); // 取得按下的鍵
if (key == L'f') { // 在遊戲中按 'f' 可以選擇立即結束本局遊戲
game_over = true;
// 如果需要在遊戲中途結束,可以考慮在這裡設置一個特殊的分數或狀態
break; // 跳出當前遊戲迴圈
}
movement(key); // 玩家移動
}
if (current_time - last_spawn_time > SPAWN_T) {
xxxyyy();
last_spawn_time = current_time; // 更新上次生成時間
}
if (current_time - last_fall_time > FALL_T) {
// 將 score 變數的引用傳遞給 moveDown 函式
bool collision_detected = moveDown(final_score); // 使用 final_score
if (collision_detected) {
game_over = true; // 設為遊戲結束
}
last_fall_time = current_time; // 更新上次掉落時間
}
// --- 最終碰撞檢查 (玩家與新落下的或現有障礙物的碰撞) ---
// 確保玩家移動到一個有 'x' 的位置時也能判定死亡
if (play_map[player.y][player.x] == L'x') {
game_over = true; // 設為遊戲結束
}
// --- 遊戲狀態更新與繪製 ---
drawMap(); // 重新繪製地圖以顯示所有更新
gotoxy(0, MAP_H + 1); // 將游標移動到地圖下方
wcout << L"你的分數: " << final_score << " " << endl; // 清空舊分數,避免殘影
// 稍微延遲一下,控制遊戲速度,避免 CPU 使用率過高
std::this_thread::sleep_for(std::chrono::milliseconds(20)); // 讓迴圈每 20 毫秒執行一次,讓遊戲更流暢
}
// --- 遊戲結束後的訊息 ---
gotoxy(0, MAP_H + 3); // 移動游標到地圖下方更遠處
wcout << L"哇哇 死掉了!你被障礙物砸中了!最終分數: " << final_score << endl;
wcout << L"遊戲結束。按 'f' 重新開始,按 'q' 退出遊戲。" << endl;
// 等待玩家輸入重新開始或退出
char choice = ' ';
while (true) {
if (_kbhit()) {
choice = _getch();
if (choice == 'f' || choice == 'F') {
play_again = true; // 玩家選擇重新開始
break;
} else if (choice == 'q' || choice == 'Q') {
play_again = false; // 玩家選擇退出
break;
}
}
std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 短暫延遲,避免佔用CPU
}
system("cls"); // 清空螢幕
}
return 0;
}
程式介紹
程式碼:
// 使用 vector<string> 比 char[][] 更方便且安全地操作地圖。
// 定義地圖尺寸
const int MAP_H = 8; // 地圖高度,從 0 到 7
const int MAP_W = 22; // 地圖寬度,從 0 到 21
// 初始地圖設定 - 這裡將玩家 ('o') 移除,會在 reset 中放置
vector<wstring> play_map = {
L"######################",
L"# #",
L"# #",
L"# #",
L"# #",
L"# #",
L"# #", // 原本的玩家位置現在也初始化為空白
L"######################" // 最後一行保持為牆壁
};
struct Player {// 玩家
int x;
int y;
} player;
bool game_over = false;// 控制遊戲狀態
int final_score = 0; // 用於儲存最終分數
程式碼:
void welcome()
{
wcout << L"\n\n\t\t\t找 洞 鑽";
wcout << L"\n\n\t\t 請按任意鍵開始";
wcout << L"\n\n\t\t 按F可結束遊戲";
getch();//得到任意鍵
system("cls");//清空螢幕
SetConsoleTitleW(L"找洞鑽");//設置窗口標題
}
void gotoxy(int x, int y) {// 設定游標位置,讓畫面更新更流暢
COORD coord;
coord.X = x;
coord.Y = y;
SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), coord);// 將游標移動到指定的位置
}
void drawMap() {
gotoxy(0, 0); // 將游標移動到左上角,以便重新繪製整個地圖
for (int i = 0; i < MAP_H; ++i) {
wcout << play_map[i] << endl;
}
}
程式碼:
void reset() {// 重置遊戲地圖和玩家位置
play_map = {
L"######################",
L"# #",
L"# #",
L"# #",
L"# #",
L"# #",
L"# #",
L"######################"
};
player.x = 11; // 玩家 X 座標重置到中心
player.y = 6; // 玩家總是在y=6上
play_map[player.y][player.x] = L'o'; // 只在這裡放置玩家 'o'
game_over = false; // 重置遊戲時,將遊戲結束狀態設為 false
final_score = 0; // 重置分數
}
程式碼:
void xxxyyy() {// 生成新的障礙物
for (int i = 1; i < MAP_W - 1; ++i) {// 先都設定為障礙物 'x'
play_map[1][i] = L'x';
}
int hole_w = rand() % 2 + 3; // 隨機生成 3 或 4 格寬的洞,因為%2
// 計算洞的起始位置範圍
int max_hole_start_x = (MAP_W - 1) - hole_w;// 確保出現在遊戲範圍裡
int hole_start_x = rand() % max_hole_start_x + 1;// +1是因為從1開始
for (int i = 0; i < hole_w; ++i) {// 挖出第一個洞
play_map[1][hole_start_x + i] = L' ';
}
if (rand() % 10 < 3) { // 30% 機率生成另外的洞
int an_hole_w = rand() % 2 + 1; // 1 或 2 格寬
int an_hole_start_x;
bool v_found = false;// 還沒找到適合的地方
// 嘗試找一個不重疊的位置
for(int attempt = 0; attempt < 10; ++attempt) { // 嘗試幾次
an_hole_start_x = rand() % (MAP_W - 1 - an_hole_w) + 1;
bool overlap = false; // 沒有重疊到
for(int i = 0; i < an_hole_w; ++i) {
if (play_map[1][an_hole_start_x + i] == ' ') { // 如果新洞的位置已經是空格,說明重疊了
overlap = true; //有重疊
break;
}
}
if (!overlap) { // 沒有重疊
v_found = true; // 找到地方了
break;
}
}
if (v_found) {
for (int i = 0; i < an_hole_w; ++i) {
play_map[1][an_hole_start_x + i] = L' ';
}
}
}
}
程式碼:
// 移動所有障礙物向下,並計算分數
bool moveDown(int& current_score) { // 傳入分數引用(&=傳入分數本身不是分身)
bool ya = false; // 沒有撞到玩家
bool scored_this_frame = false; // 標記這一幀是否有分數增加
// 從底部往上遍歷所有可移動的行(不包括邊界)
for (int r = MAP_H - 2; r >= 1; --r) { // 從玩家上方一行開始往上檢查(8-2=6=r)
for (int c = 1; c < MAP_W - 1; ++c) { // 遍歷所有可移動的列
if (play_map[r][c] == L'x') { // 如果當前位置有障礙物
if (r + 1 == player.y && c == player.x) {// 檢查下方是否為玩家
ya = true; // 撞到了
}
// 將障礙物移動到下一行,只有當下一行不是最底部的牆壁時才移動
if (r + 1 < MAP_H - 1) { // 確保障礙物不會覆蓋到最底下那行
play_map[r + 1][c] = L'x';
// 如果障礙物從玩家上方一行成功移動到最後一行,並且沒有撞到 (沒有 ya),則加分
if (r == player.y - 1 && !ya) {
if (!scored_this_frame) { // 確保次只加一次分數
current_score += 1; // 增加分數
scored_this_frame = true;
}
}
}
play_map[r][c] = L' ';// 清除原位置
}
}
}
return ya; // 返回是否發生碰撞
}
程式碼:
void movement(char move_letter) {// 處理玩家移動
play_map[player.y][player.x] = L' '; // 先將玩家目前位置清空
switch (move_letter) {
case 'a': // 向左移動
// 檢查是否超出左邊界以及目標位置是否為空(非牆壁且非障礙物)
if (player.x > 1 && play_map[player.y][player.x - 1] == L' ') {
player.x--;
}
break;
case 'd': // 向右移動
// 檢查是否超出右邊界以及目標位置是否為空(非牆壁且非障礙物)
if (player.x < MAP_W - 2 && play_map[player.y][player.x + 1] == L' ') {
player.x++;
}
break;
case 'f': // 重置遊戲
// 這裡不再直接調用 resetGame,因為還有遊戲結束和或重置的選擇
// 如果在遊戲結束後按 'f' 應該觸發重啟
if (game_over) {
// 在 main 迴圈外處理重置
}
break;
}
play_map[player.y][player.x] = L'o'; // 在新位置放置玩家
}
程式碼:
int main() {
// START: 確保控制台能正確顯示 UTF-8 中文
// 設置標準輸出流模式為 UTF-8 文本
_setmode(_fileno(stdout), _O_U8TEXT);
// 設置標準輸入流模式為 UTF-8 文本 (如果需要處理中文輸入的話)
_setmode(_fileno(stdin), _O_U8TEXT);
// 設置控制台輸出代碼頁為 UTF-8 (65001)
SetConsoleOutputCP(CP_UTF8);
// 設置控制台輸入代碼頁為 UTF-8 (65001) (如果需要處理中文輸入的話)
SetConsoleCP(CP_UTF8);
// END: 確保控制台能正確顯示 UTF-8 中文
welcome();
srand(static_cast<unsigned int>(time(nullptr))); // 利用時間設定亂數種子
bool play_again = true;
程式碼:
while (play_again) { // 外部迴圈控制遊戲是否重啟
reset(); // 遊戲開始前先重置地圖和玩家位置
final_score = 0; // 每次重置遊戲時,分數也要重置
long long last_spawn_time = GetTickCount(); // 記錄上次生成障礙物的時間
long long last_fall_time = GetTickCount(); // 記錄上次障礙物掉落的時間
// 每 1800 毫秒 (1.8秒) 生成一次障礙物
const int SPAWN_T= 1800;
// 每 600 毫秒 (0.6秒) 障礙物掉落一次
const int FALL_T = 600;
drawMap(); // 初始繪製地圖
while (!game_over) { // 當 game_over 為 true 時跳出迴圈
long long current_time = GetTickCount(); // 取得當前時間
if (_kbhit()) { // 檢查是否有按鍵被按下
char key = _getch(); // 取得按下的鍵
if (key == L'f') { // 在遊戲中按 'f' 可以選擇立即結束本局遊戲
game_over = true;
// 如果需要在遊戲中途結束,可以考慮在這裡設置一個特殊的分數或狀態
break; // 跳出當前遊戲迴圈
}
movement(key); // 玩家移動
}
if (current_time - last_spawn_time > SPAWN_T) {
xxxyyy();
last_spawn_time = current_time; // 更新上次生成時間
}
if (current_time - last_fall_time > FALL_T) {
// 將 score 變數的引用傳遞給 moveDown 函式
bool collision_detected = moveDown(final_score); // 使用 final_score
if (collision_detected) {
game_over = true; // 設為遊戲結束
}
last_fall_time = current_time; // 更新上次掉落時間
}
// --- 最終碰撞檢查 (玩家與新落下的或現有障礙物的碰撞) ---
// 確保玩家移動到一個有 'x' 的位置時也能判定死亡
if (play_map[player.y][player.x] == L'x') {
game_over = true; // 設為遊戲結束
}
// --- 遊戲狀態更新與繪製 ---
drawMap(); // 重新繪製地圖以顯示所有更新
gotoxy(0, MAP_H + 1); // 將游標移動到地圖下方
wcout << L"你的分數: " << final_score << " " << endl; // 清空舊分數,避免殘影
// 稍微延遲一下,控制遊戲速度,避免 CPU 使用率過高
std::this_thread::sleep_for(std::chrono::milliseconds(20)); // 讓迴圈每 20 毫秒執行一次,讓遊戲更流暢
}
程式碼:
// --- 遊戲結束後的訊息 ---
gotoxy(0, MAP_H + 3); // 移動游標到地圖下方更遠處
wcout << L"哇哇 死掉了!你被障礙物砸中了!最終分數: " << final_score << endl;
wcout << L"遊戲結束。按 'f' 重新開始,按 'q' 退出遊戲。" << endl;
// 等待玩家輸入重新開始或退出
char choice = ' ';
while (true) {
if (_kbhit()) {
choice = _getch();
if (choice == 'f' || choice == 'F') {
play_again = true; // 玩家選擇重新開始
break;
} else if (choice == 'q' || choice == 'Q') {
play_again = false; // 玩家選擇退出
break;
}
}
std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 短暫延遲,避免佔用CPU
}
system("cls"); // 清空螢幕
}
return 0;
}
反思與展望
心得與反思
- 在這次製作過程,遇到很多麻煩,有適時的運用到AI協助,而在經過AI調整後。都有慢慢去看每個部分,可以很快速的達成我原本想很久的問題。
- 整個遊戲其實整體改變不少,原先是想製作成炸彈掉下來,要閃躲的感覺,但後來覺得現在這種方式比較簡單。
遇到的問題
一開始遇到的問題是障礙物無法正常掉落,於是我想到運用自訂義涵式。後來又遇到中文無法正常顯示的問題,於是去找Gemini幫忙,它建議使用UTF-8。
但在最後遊玩的時候都會覺得太快,加上延遲及時間,讓它下落的速度與出現速度得到控制
成果反思
這次製作遇到很多問題,因為學期末的關係導致較晚開始,再加上對於自定義函式不算特別熟悉,導致整體製作時間拉長
這次成發讓我們學到了如何把課堂知識應用在實際操作中,也增進了團隊合作的默契。看到成果順利完成,我們都很開心,感謝學姐們的耐心指導。
未來展望
- 可以在遊戲裡面添加計時器的功能,讓玩家知道自己玩了多長時間
- 添加歷史最高分數,這樣可以讓玩家為了打破紀錄玩更久
- 以關卡數作為判斷標準,加快掉落速度及數量
謝謝大家🩶
Copy of Copy of Minimal
By xuyijie
Copy of Copy of Minimal
- 9