的黑框小遊戲

C++

講師介紹

  • 景美電資32nd教學

  • 姓名:黃語涵

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

  • 性別:女

  • 狀態:醒著但快睡著了

課程概要

  1. C++語法

  2. 黑框小遊戲架構

課程目標

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

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

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

會一步步帶你

今日嘉賓:

寒暑假不限時復刻

優點

  1. APCS愛用
  2. 長姐愛用
  3. 海的女兒愛用

缺點

  1. 下載麻煩
  2. 設定麻煩
  3. 打開麻煩

但在真正強者面前,只算缺點你

點它

點它們

怎麼執行

build and run

檢查檔案是否變更/檢查語法是否正確

執行程式

C++語法複習

  • 二維陣列

  • vector

  • 自訂義函式

  • struct

  • switch

二維陣列

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

它在程式裡的資料型態

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

如果換個排版方式

是不是覺得有點像有長x寬的櫃子呢?

簡單來說

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

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

通常會用雙重迴圈存取

C++ VS. python世界大戰

int arr[5][10]={};

C++

Python

arr=[[0]*10 for i in range(5)]
cout<<arr[3][2];//0
print(arr[3][2])#0

主要差別在宣告二維陣列

you can try try看

輸入 :

 

輸出 :

 

1 2 3 4
5 6 7 8
8 7 6 5 
4 3 2 1
#include<iostream>
using namespace std;
int main(){
int arr[2][4]={};
for (int i=0;i<2;i++){
	for(int j=0;j<4;j++){
    	cin>>arr[i][j];
    }
}
for(int i=1;i>=0;i--){
	for(int j=3;j>=0;j--){
    	cout<<arr[i][j]<<" ";
	}
    cout<<endl;
}
return 0;}

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()     | 刪除某位置或某區間|

C++ VS. python世界大戰

python

C++

arr = [1, 2, 3] 
arr.append(4)
len(arr)
arr.pop()
vector<int> v = {1, 2, 3};
v.push_back(4);
v.size()
v.pop_back();

自訂義涵式

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

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

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

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

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

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

函式

程式碼可以重複使用

  • 不用一段程式碼一直「複製貼上」。

讓程式更簡潔、可讀性更高

  • 把複雜問題拆成多個小任務,每個小任務用一個函式處理。

  • 看程式時,不需要深入看每行細節,只要看「這個函式叫什麼名字」就知道功能。

方便維護和修改

  • 如果程式需要修改,只要改 函式內部的程式碼。

 提高程式的結構化與模組化

  • 函式讓你的程式像拼積木一樣,一塊一塊組成,容易管理。

結構

沒有參數沒有回傳值

void 函式名(){
	//程式碼;
    }

有參數沒有回傳值

void 函式名(宣告 變數){
	//程式碼;
    }

結構

有參數有回傳值

宣告 函式名(){ //根據要回傳的東西宣告資料型態
	//程式碼;
    return 咚咚;
    }

全域整數

宣告 變數;
int main(){
//主程式;
}

C++ VS. python世界大戰

python

C++

def 函式名(參數):
	#程式碼
    return 222
int 函式名(參數){
	//程式碼
    return 222;}

練習:輸出hello world

#include<iostream>
using namespace std;
void hello(){
    cout<<"hello world";
}
int main(){
hello();
return 0;}

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++ VS. python世界大戰

x = "hoyouu"

match x:
    case "Yuan Peng":
        print("her beloved")
    case "Viviam":
        print("her beloved")
    case "Pin Ying":
        print("her beloved")
    case _:  # 預設情況 (default)
        print("she will love")
string x="hoyouu";

switch(x){
      case "Yuan Peng":
        cout<<"her beloved"
        break;
    case "Viviam":
        cout<<"her beloved"
        break;
    case "Pin Ying":
        cout<<"her beloved"
        break;
    default:  
        cout<<"she will love";
        break;
}

python

C++

核心概念

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

底層原理

應用

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

  • 資料結構(狀態)

    • 玩家座標:playerX, playerY

    • 地圖:vector<string> map;

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

  • 輸入處理

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

  • 遊戲邏輯更新

    • 玩家移動

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

    • 分數、血量更新

  • 畫面更新(Render)

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

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

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

黑框小遊戲

前置作業

  • 繪製地圖
  • struct

底層概念

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

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

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

字元字串要導入標頭檔:#include<string>

做好預制菜

依照遊戲規劃分類欲移動項,用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. 開頭時可以讓玩家讀完規則後按任意鍵開始遊戲

( y ,x )

( y +1, x )

( y -1, x)

( y , x-1 )

( y, x+1 )

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<r;i++){
        for(int j=1;j<c;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]);
    }
}

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

接下來試驗牌環節

開始實作

我要驗牌

牌沒問題

給我擦皮鞋

鳥都不鳥你

煮完程式後

就可以開始吃飯了耶!!!!!!

耶一耶一耶一耶喔喔

謝謝大家

C++的黑框小遊戲

By gg dd

C++的黑框小遊戲

  • 140