的黑框小遊戲

C++

講師介紹

  • 景美電資32nd教學

  • 姓名:黃語涵

  • 物種:動物界脊索動物門哺乳綱靈長目人科人屬智人種

  • 性別:女

  • 狀態:活著且有呼吸但快沒了

課程概要

  1. C++語法

  2. 黑框小遊戲架構

課程目標

  • 掌握遊戲迴圈 (Game Loop):理解「輸入處理 => 邏輯更新 =>畫面渲染 => 時間控制」的標準流程。

  • 熟悉 STL Vector 應用:學習如何管理動態物件(如子彈、怪物)的生成與銷毀。

  • 控制台繪圖技巧:學習如何利用游標控制來解決畫面閃爍問題。

會一步步帶你

C++語法複習

  • 二維陣列

  • vector

  • 自訂義函式

  • struct

  • switch

二維陣列

       何謂二維陣列於記憶體中的配置方式?其實二維陣列存取時的行與列,只是為了便於理解陣列元素索引。如果要大量儲存同一種型態、而且彼此又有密切關係的「表格式」資料,例如數學中的矩陣,這時候就應將其宣告並設定為「二維陣列」。本質是在陣列裡的陣列。

int arr[3][3]={
{1,2,3},
{4,5,6},
{7,8,9}
};

簡單來說

i

j

0

1

2

3

0

1

2

3

0-0

0-1

0-2

0-3

3-0

3-1

3-2

3-3

1-0

1-1

1-2

1-3

2-0

2-1

2-2

2-3

j

i

你只要會去蝦皮智取店領貨你就會二維陣列的存取!

通常會用雙重迴圈存取

Vector

  1. vector 是 C++ 中最常用的動態陣列容器。
  2. 元素可自動擴張(不像一般陣列大小固定)

  3. 支援隨機存取

  4. 插入、刪除、搜尋都很方便

  5. 需要 #include <vector>

📌 如何宣告 vector?

vector<int> v;

vector<int> v(5);
// 大小 5,每個元素預設為 0

vector<int> v(5, 10);
// 大小 5,每個值都是 10
| 操作        | 函式         |     說明        |
| ------------|-------------| ---------------|
| 新增到尾端   | push_back(x)| 加在最後         |
| 移除尾端     | pop_back() | 刪除最後一個     |
| 判斷是否為空 | empty()     | true/false      |
| 清空所有元素 | clear()     | 全刪除           |
| 插入        | insert()    | 插入到某個位置    |
| 刪除        | erase()     | 刪除某位置或某區間|

自訂義涵式

可重複使用的程式區塊,它能執行特定的任務,並且可以把結果回傳給呼叫它的地方。

  • 有名字 – 方便呼叫(像叫某人名字一樣)。

  • 有輸入(參數) – 你可以把資料交給函式處理。

  • 有回傳值 – 處理完的結果可以交回給主程式(也可以不回傳)。

  • 能重複使用 – 寫一次,可以呼叫多次。

回傳型別 函式名稱(參數列表) {
    // 執行邏輯
    return 回傳值;
}

Struct

struct是 C++ 用來「打包資料」最強大的基礎工具。

// 定義一個叫做 club 的結構
struct club {
    // 這些是成員變數 
    string name;   // 名字
    int num;        // 座號
    string class;   // 班級
    }; // <=== 注意!這裡一定要有分號 ;

使用 Struct 的寫法 (打包管理)就像將一堆散亂的資料打包整理好

    // 方法 A:逐一填寫 (最清楚)
    club s1;        // 產生一個社員變數 s1 
    s1.name = "江沛慈";  // 用「點 (.)」來存取裡面的欄位
    s1.num = 4;
    s1.class = "Ren";

    // 方法 B:快速初始化 (類似填表單,順序要對)
    club s2 = { "劉姿瑩", 27, "Ren" };
    cout<<s2.num;//27
    return 0;
}

switch &Case

類似if判斷式,但更快更簡潔

switch(變數):
    case 1: //if(變數==1)
    	//想執行的程式碼
        break;
    case 2://else if
        break;
    default://else
    	break;

如果沒有break,他就會無視case的條件繼續跑下去

核心概念

「C++ 黑框小遊戲」的本質,就是在文字主控台裡跑一個遊戲迴圈,不斷:讀輸入 → 更新狀態 → 重畫畫面,然後用字元把畫面「演」出來。

底層原理

應用

貪吃蛇、迷宮、打怪、射擊、RPG、小平台跳躍等

  • 資料結構(狀態)

    • 玩家座標:playerX, playerY

    • 地圖:vector<string> map;

    • 敵人位置、子彈列表、血量等等

  • 輸入處理

    • 讀鍵盤(WASD、空白鍵…)

  • 遊戲邏輯更新

    • 玩家移動

    • 碰撞判定(撞牆、撞怪、撿道具)

    • 分數、血量更新

  • 畫面更新(Render)

    • 把當前狀態轉成一個「字串地圖」

    • 清畫面 → 再印出新的畫面

大部分黑框小遊戲核心結構

黑框小遊戲

今天以射擊遊戲作為範例

Coco

前置作業

  • 繪製地圖
  • struct

底層概念

利用二維陣列字串繪製地圖,用一個字元圍出陣列的圍牆,然後剩下用全形空白字符儲存作為玩家移動的區域。

char play_map[r][c] = {
"######################",
"#                    #",
"#                    #",
"#                    #",
"#                    #",
"#                    #",
"#                    #",
"#                    #",
"#                    #",
"#                    #",
"#                    #",
"#                    #",
"#                    #",
"#                    #",
"######################"
};

示意圖,地圖的長寬一陣列大小決定

做好預制菜

依照遊戲規劃出陣營,用struct整理宣告

ex:

敵方單位位置

struct Monster {
    int x,y;
};

子彈發射位置

struct Bullet {
    int x,y;
};

建立陣列存放這些東西的位置,作為索引表

vector<Bullet> bullets;
vector<Monster> monsters;

開始備料:函式

函式:控制玩家移動

step1.選一個喜歡的字元作為主控加到地圖中

(當然想要用超過一個字元也可以只是更麻煩

char play_map[長][寬] = {
"######################",
"#                    #",
"#                    #",
"#                    #",
"#                    #",
"#                    #",
"#                    #",
"#                    #",
"#                    #",
"#                    #",
"#                    #",
"#                    #",
"#                    #",
"#         I          #",
"######################"
};

選一個地方做玩家的初始位置

舉個栗子

step2.用函式讀取玩家按的按鍵

getch()//讀取任意鍵並回傳
#include<conio.h>

要導入的函式庫

  1. 讀取玩家欲移動的方向作為參數放到函式
  2. 開頭時可以讓玩家讀完規則後按任意鍵開始遊戲

step3.寫一個自訂義函式功能是控制主控移動

void movement(direction){//沒用回傳值所以用void
	switch (direction){
    	case 'w'://向上移一格
        	if (player.y>1)player.y--;//避免玩家超過圍牆
            break;
        case 'a'://向左移一格
        	if (player.x>1)player.x--;
            break;
        case 's'://向下移一格
        	if (player.y<r))player.y++;
            break;
        case 'd'://向右移一格
        	if (player.x<c)player.x++;
            break;
    }

當然,你要寫何晨光的移動方式也很棒👍

不管你按哪個鍵,電腦都能直接跳到那一行執行,不用一個一個問

怎麼處理敵方單位

如接掉落物或射擊遊戲需要在最上面刷新單位並隨時間往下移

Step 1.所有怪物往下掉 (位移邏輯)

叫出 monsters 陣列裡的每一隻怪物,把它的 y 座標加 1=>向下移動一格

for (auto &m : monsters)
    m.y++;

auto 自動幫你算出 m 的資料型態

  • 有寫 & ( auto &m ): 你拿到的是怪物的「本尊」。 當你執行 m.y++ 時,陣列裡的那隻怪物真的會往下移。
  • 沒寫 & ( auto m ): 你拿到的是「影印本 」。 系統會複製一隻一模一樣的怪物給你,你執行 m.y++,是影印本往下移,迴圈結束後影印本被丟掉,陣列裡的本尊還是在原地發呆。

Step 2.刪除超出地圖的怪物,扣血

查每一隻怪物,如果它超出邊界,就扣玩家的血 ,並刪除這隻怪物

monsters.erase(
    remove_if(monsters.begin(), monsters.end(),[&](Monster m){
        if (m.y >= r){   // 避免怪物跑出地圖
            hp--;         // 懲罰:扣玩家血量
            return true;  // 回傳這隻要刪掉
        }
        return false;     // 回傳這隻留著
    }),
    monsters.end()
);

函式名稱( 資料1 , 資料2 , 資料3 );

  1. 從哪裡開始? (起點)
  2. 到哪裡結束? (終點)
  3. 刪除規則是什麼? (條件)

Step 3.生成新怪物 (生成邏輯)

查每一隻怪物,如果它超出邊界,就扣玩家的血 ,並刪除這隻怪物

Monster newM;  //產生一個Monster變數 newM 
newM.x = rand() % 18 + 2; // 2~19 
newM.y = 1;
monsters.push_back(newM);

根據需求讓設定隨機生成的範圍

把它放在陣列裡方便後續尋找&控制

#include <ctime>
#include <algorithm>
void refresh_monster()
{
    // 1. 所有怪物往下掉
    for (auto &m : monsters)
        m.y++;

    // 2. 刪除超出地圖的怪物,扣血
    monsters.erase(
        remove_if(monsters.begin(), monsters.end(),
        [&](Monster m){
            if (m.y >= 21){
                hp--;
                return true;
            }
            return false;
        }),
        monsters.end()
    );

    // 3. 生成新怪物
    Monster newM;
    newM.x = rand() % 18 + 2; // 2~19
    newM.y = 1;
    monsters.push_back(newM);
}

怎麼發射子彈

1.怎麼發射子彈

偷偷在getch那邊加入一個控制:案按??鍵執行發射子彈的函式

char c = getch();
if (c=='a' && playerX > 1)playerX--;
else if (c=='d' && playerX < c)playerX++;
else if (c==' ')shoot();

如果真的不習慣用switch,可以用if(人操作速度太慢對程式執行速度沒太大差距

當按空格時,會執行"shoot()"這個自訂義函式

2.紀錄子彈要生成的位子

偷偷在getch那邊加入一個控制:按??鍵執行發射子彈的函式

如果真的不習慣用switch,可以用if(人操作速度太慢對程式執行速度沒太大差距

struct Bullet {
    int x,y;
};
void shoot()
{
    Bullet b;
    b.x = playerX;
    b.y = playerY - 1;//在玩家頭上一格生成子彈
    bullets.push_back(b);//將子彈的座標存放在一起處理
    Beep(400, 50);//發出400Hz聲音持續50ms
}

需導入標題檔

#include <windows.h>

子彈後續處理

1.子彈的運動軌跡

// 如果子彈往上
for (auto &b : bullets)//邏輯跟前面一樣
b.y--;

依照慣性,那就會依直往前飛

但遊戲不一定要遵循物理,可以打破規則不走尋常路

Coco

2.判定有沒有打中敵方單位

for (auto &b : bullets) {      // 外層迴圈:拿出每一顆子彈
    for (auto &m : monsters) { // 內層迴圈:拿出每一隻怪物

命中判定 (座標重疊)

if (b.x == m.x && b.y == m.y) {

阿總之  窮舉

處理後事 (標記刪除)

	Beep(400, 50);//命中音效
    m.y = 999; // 把怪物踢到地圖最下面 (標記死亡)
    b.y = -1;  // 把子彈踢到地圖最上面 (標記失效)
    score++;   // 加分
}

⚠️為什麼不直接刪除 (erase)? 你在跑迴圈的時候,如果突然把那一格抽掉 ,整個隊伍順序會亂掉,程式會當機。所以先發配邊疆!

3.處理邊疆的流臣

monsters.erase(
        remove_if(monsters.begin(), monsters.end(), [](Monster m){
            return m.y > r;
        }),
        monsters.end()
    );

如果在邊疆,回傳true,告訴劊子手執行

一路砍到最後

4.清理子彈

bullets.erase(
    remove_if(bullets.begin(), bullets.end(), [](Bullet b){
        return b.y < 1;
    }),
    bullets.end()
    );

將跑到外太空失聯的子彈座標刪除

那你知道什麼東西永遠不會失聯,可以減少程式的工程量

因為

void update_bullets()
{
    // 子彈往上
    for (auto &b : bullets)
        b.y--;

    // 打中怪物
    for (auto &b : bullets) {
        for (auto &m : monsters) {
            if (b.x == m.x && b.y == m.y) {
                m.y = 999; // 標記刪除
                b.y = -1; // 標記刪除
                score++;
            }
        }
    }

    // 清理怪物
    monsters.erase(
        remove_if(monsters.begin(), monsters.end(), [](Monster m){
            return m.y > 30;
        }),
        monsters.end()
    );

    // 清理子彈
    bullets.erase(
        remove_if(bullets.begin(), bullets.end(), [](Bullet b){
            return b.y < 1;
        }),
        bullets.end()
    );
}

休息

輸出地圖

void draw_map()
{
    // 全部清空中間區域
    for(int i=1;i<21;i++){
        for(int j=1;j<21;j++){
            play_map[i][j] = ' ';
        }
    }
    // 玩家
    play_map[playerY][playerX] = 'I';

    // 子彈
    for (auto b : bullets) {
        if (b.y >= 0 && b.y < H && b.x >= 0 && b.x < W)
            play_map[b.y][b.x] = '|';
    }

    // 怪物
    for (auto m : monsters) {
        if (m.y > 0 && m.y < r)
            play_map[m.y][m.x] = 'E';
    }
}

更新地圖

選擇喜歡的東東當它在遊戲中的樣子

void print_map()
{
    for(int i=0;i<H;i++){
        puts(play_map[i]);
    }
}

輸出地圖

比cout更快,我們要追求奧運精神

組裝原料:煮程式

前情提要

 srand(time(0));//設定時間種子
    cout << "按任意鍵開始遊戲..." << endl;
    getch();//玩家準備好再開始開始

初始介面:根據遊戲的需求客製化

while (hp > 0)//遊戲執行的條件
    {
        // 移動輸入處理
        if (kbhit()) { //如果操作者按任意鍵
            char direction = getch();
            movement(direction);
            }
        // 怪物每 1 秒刷新一次
        if (monsterTimer >= 10) {  // 每回合 100ms → 10 回合 = 1秒
            refresh_monster();
            monsterTimer = 0;
        }
        // 更新子彈
        update_bullets();
        // 更新地圖
        draw_map();
        print_map();
        monsterTimer++;
        Sleep(100); // 每 100ms 一回合
    }

前情提要

while (hp > 0)//遊戲執行的條件
    {
        // 移動輸入處理
        if (kbhit()) { //如果操作者按任意鍵
            char de = getch();
            movement(direction);
        // 怪物每 1 秒刷新一次
        if (monsterTimer >= 10) {  // 每回合 100ms → 10 回合 = 1秒
            refresh_monster();
            monsterTimer = 0;
        }
        // 更新子彈
        update_bullets();
        // 更新地圖
        draw_map();
        print_map();
        monsterTimer++;
        Sleep(100); // 每 100ms 一回合
    }

如果沒有if 那只要操作者不按任何按鍵程式就會卡在getch()

所以我們可以照這個邏輯搞出一個暫停鍵出來

現在有沒有覺得我們程式像星星,一閃一閃亮晶晶

接下來要教你們這段程式碼是 Console(控制台)遊戲開發中含金量最高的技術之一。

void Set()//把每次光標移動到初始位置
{
    HANDLE hOut;
    COORD pos={0,0};//設置控制台光標位置回到 ( 0, 0 )【之後才能打印地圖】
    hOut = GetStdHandle(STD_OUTPUT_HANDLE);//得到 標準輸出的句柄
    SetConsoleCursorPosition(hOut,pos);
    CONSOLE_CURSOR_INFO cursorInfo = {1, FALSE};//把光標關掉
    SetConsoleCursorInfo(hOut, &cursorInfo);
}
#include <windows.h>

需導入函式庫(前面有導入過了)

void Set()//把每次光標移動到初始位置
{
    HANDLE hOut;
    COORD pos={0,0};//設置控制台光標位置回到 ( 0, 0 )【之後才能打印地圖】
    hOut = GetStdHandle(STD_OUTPUT_HANDLE);//得到 標準輸出的句柄
    SetConsoleCursorPosition(hOut,pos);
    CONSOLE_CURSOR_INFO cursorInfo = {1, FALSE};//把光標關掉
    SetConsoleCursorInfo(hOut, &cursorInfo);
}

HANDLE hOut;

白話文: 宣告一個「遙控器」。

解釋: 在 Windows 系統裡,你要操作任何視窗(Window)、檔案或設備,都需要一個「識別證」,這個就叫做 Handle (控制代碼/句柄)。這裡我們準備一個變數 hOut 來存放等等要拿到的遙控器。

GetStdHandle(STD_OUTPUT_HANDLE);

白話文: 跟 Windows 系統說:「把『螢幕畫面 (Output)』的遙控器交給我!」

解釋:

STD_INPUT_HANDLE:鍵盤(輸入)。

STD_OUTPUT_HANDLE:螢幕(輸出)<-- 我們要這個。

拿到這個遙控器 (hOut) 之後,我們才能命令螢幕做事情(比如移動游標、改變顏色)。

void Set()//把每次光標移動到初始位置
{
    HANDLE hOut;
    COORD pos={0,0};//設置控制台光標位置回到 ( 0, 0 )【之後才能打印地圖】
    hOut = GetStdHandle(STD_OUTPUT_HANDLE);//得到 標準輸出的句柄
    SetConsoleCursorPosition(hOut,pos);
    CONSOLE_CURSOR_INFO cursorInfo = {1, FALSE};//把光標關掉
    SetConsoleCursorInfo(hOut, &cursorInfo);
}

COORD pos={0,0};

白話文: 設定導航目的地為 (0, 0),也就是「左上角」。

解釋: COORD 是 Windows 定義的一個結構 (Struct),裡面只有 X 和 Y。

(0, 0) 是控制台視窗的最左上角。

SetConsoleCursorPosition(hOut, pos);

白話文: 用剛剛拿到的遙控器 (hOut),把游標瞬移到 pos (0,0) 的位置。

主管的技術重點(為什麼不清除螢幕?):

一般新手會用 system("cls"),那是把黑板擦乾淨再重寫 -> 會閃爍。

高手用這行,是不擦黑板,直接把粉筆移到最開頭,覆蓋 (Overwrite) 舊的圖案 -> 完全不閃,像動畫一樣流暢。

void Set()//把每次光標移動到初始位置
{
    HANDLE hOut;
    COORD pos={0,0};//設置控制台光標位置回到 ( 0, 0 )【之後才能打印地圖】
    hOut = GetStdHandle(STD_OUTPUT_HANDLE);//得到 標準輸出的句柄
    SetConsoleCursorPosition(hOut,pos);
    CONSOLE_CURSOR_INFO cursorInfo = {1, FALSE};//把光標關掉
    SetConsoleCursorInfo(hOut, &cursorInfo);
}

CONSOLE_CURSOR_INFO cursorInfo = {1, FALSE};

白話文: 填寫一張「游標設定表」。

解釋: 這個結構有兩個欄位:

dwSize (1):游標的厚度(1~100%)。設多少沒差,因為我們要隱藏它。

bVisible (FALSE):是否看見? 設定為 FALSE 就是隱藏。

SetConsoleCursorInfo(hOut, &cursorInfo);

白話文: 把這張設定表交給系統執行。

細節: 注意這裡要用 &cursorInfo,因為 Windows API 通常需要傳入記憶體位址(指標)才能讀取你的設定。

void print_map()
{
    Set();
    for(int i=0;i<H;i++){
        puts(play_map[i]);
    }
}

把它塞在這裡就可以解決了

繼續延伸

敵方單位多元化

step1.創建敵方單位庫

// (受害者名單)
const vector<string> 陣列名= {
    "Hoyouu",  
    "Jamie", 
    "姻緣朋",  
    "攝長",   
    "Coco", 
    "Ktlai",   
    "Terry",
    "Vivian",
    "sevenseven"
};

敵方單位多元化

step2.偷偷的調整struct

struct Monster {
    int x, y;
    string name;//受害者名
    int width;//名字長度
};

敵方單位多元化

step3.調整函式負責控制生成敵方單位的地方

Monster newM;
    int randIndex = rand() % 怪物名.size();
    newM.name = 受害者卡池[randIndex];//從池子隨機抓一個人
    newM.width = newM.name.length();//記錄他的長度

    int safeMaxX = c-2-newM.width;//確保他部會超界頭身分離
    if (safeMaxX < 2) safeMaxX = 2;

    newM.x = rand() % safeMaxX + 2;
    newM.y = 1;
    monsters.push_back(newM);
Monster newM;
    int randIndex = rand() % 怪物名.size();
    newM.name = 受害者卡池[randIndex];//從池子隨機抓一個人
    newM.width = newM.name.length();//記錄他的長度

    int safeMaxX = c-2-newM.width;//確保他部會超界頭身分離
    if (safeMaxX < 2) safeMaxX = 2;

    newM.x = rand() % safeMaxX + 2;
    newM.y = 1;
    monsters.push_back(newM);

敵方單位多元化

step3.偷偷的調整刷新敵方單位的邏輯

// 3. 生成新怪物
Monster newM;
int randIndex = rand() % SUBJECT_POOL.size();
newM.name = 受和者名單[randIndex];//隨機從卡池抽一個受害者
newM.width = newM.name.length();//受害者名稱長度

int safeMaxX=W-2-newM.width;
if (safeMaxX<2) safeMaxX=2;

newM.x = rand()%safeMaxX+2;
newM.y = 1;
monsters.push_back(newM);

敵方單位多元化

step3.偷偷的調整刷新敵方單位的邏輯

void update_bullets()
{
    for (auto &b : bullets) b.y--;

    for (auto &b : bullets) {
        if (b.y < 0) continue;
        for (auto &m : monsters) {
            if (m.y == 999) continue;
            if (b.y == m.y && b.x >= m.x && b.x < (m.x + m.width)) {
                m.y = 999;
                b.y = -1;
                score += 3;
                break;
            }
        }
    }

增加這一段

謝謝大家

C++的黑框小遊戲

By gg dd

C++的黑框小遊戲

  • 82