的黑框小遊戲
C++

講師介紹
-
景美電資32nd教學
-
姓名:黃語涵
-
物種:動物界脊索動物門哺乳綱靈長目人科人屬智人種
-
性別:女
-
狀態:醒著
但快睡著了



課程概要
-
C++語法
-
黑框小遊戲架構
課程目標
-
掌握遊戲迴圈 :理解「輸入處理 => 邏輯更新 =>畫面渲染 => 時間控制」的流程。
-
熟悉 STL Vector 應用:學習如何管理動態物件(如子彈、怪物)的生成與銷毀。
-
控制台繪圖技巧:學習如何利用游標控制來解決畫面閃爍問題。
會一步步帶你




今日嘉賓:

寒暑假不限時復刻

優點
- APCS愛用
- 長姐愛用
- 海的女兒愛用
缺點
- 下載麻煩
- 設定麻煩
- 打開麻煩


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


點它

點它們
怎麼執行

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];//0print(arr[3][2])#0主要差別在宣告二維陣列
you can try try看

輸入 :
輸出 :
1 2 3 4
5 6 7 88 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
-
vector是 C++ 中最常用的動態陣列容器。 -
元素可自動擴張(不像一般陣列大小固定)
-
支援隨機存取
-
插入、刪除、搜尋都很方便
-
需要
#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 222int 函式名(參數){
//程式碼
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>要導入的函式庫
- 讀取玩家欲移動的方向作為參數放到函式
- 開頭時可以讓玩家讀完規則後按任意鍵開始遊戲

( 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 );
- 從哪裡開始? (起點)
- 到哪裡結束? (終點)
- 刪除規則是什麼? (條件)

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









