掌握遊戲迴圈 :理解「輸入處理 => 邏輯更新 =>畫面渲染 => 時間控制」的流程。
熟悉 STL Vector 應用:學習如何管理動態物件(如子彈、怪物)的生成與銷毀。
控制台繪圖技巧:學習如何利用游標控制來解決畫面閃爍問題。
寒暑假不限時復刻
優點
缺點
但在真正強者面前,只算缺點你
點它
點它們
build and run
檢查檔案是否變更/檢查語法是否正確
執行程式
二維陣列
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
j
i
你只要會去蝦皮智取店領貨你就會二維陣列的存取!
通常會用雙重迴圈存取
C++ VS. python世界大戰
int arr[5][10]={};arr=[[0]*10 for i in range(5)]cout<<arr[3][2];//0print(arr[3][2])#0主要差別在宣告二維陣列
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 是 C++ 中最常用的動態陣列容器。元素可自動擴張(不像一般陣列大小固定)
支援隨機存取
插入、刪除、搜尋都很方便
需要 #include <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() | 刪除某位置或某區間|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(){
//主程式;
}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是 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;
}類似if判斷式,但更快更簡潔
switch(變數){
case 1: //if(變數==1)
//想執行的程式碼
break;
case 2://else if
break;
default://else
break;}如果沒有break,他就會無視case的條件繼續跑下去
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)
把當前狀態轉成一個「字串地圖」
清畫面 → 再印出新的畫面
利用二維陣列字串繪製地圖,用一個字元圍出陣列的圍牆,然後剩下用全形空白字符儲存作為玩家移動的區域。
char play_map[r][c] = {
"######################",
"# #",
"# #",
"# #",
"# #",
"# #",
"# #",
"# #",
"# #",
"# #",
"# #",
"# #",
"# #",
"# #",
"######################"
};示意圖,地圖的長寬一陣列大小決定
字元字串要導入標頭檔:#include<string>
依照遊戲規劃分類欲移動項,用struct整理宣告
敵方單位位置
struct Monster {
int x,y;
};子彈發射位置
struct Bullet {
int x,y;
};建立陣列存放這些東西的位置,作為索引表
vector<Bullet> bullets;
vector<Monster> monsters;(當然想要用超過一個字元也可以只是更麻煩
char play_map[長][寬] = {
"######################",
"# #",
"# #",
"# #",
"# #",
"# #",
"# #",
"# #",
"# #",
"# #",
"# #",
"# #",
"# #",
"# I #",
"######################"
};選一個地方做玩家的初始位置
舉個栗子
getch()//讀取任意鍵並回傳#include<conio.h>要導入的函式庫
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;
}當然,你要寫何晨光的移動方式也很棒👍
不管你按哪個鍵,電腦都能直接跳到那一行執行,不用一個一個問
如接掉落物或射擊遊戲需要在最上面刷新單位並隨時間往下移
叫出 monsters 陣列裡的每一隻怪物,把它的 y 座標加 1=>向下移動一格
for (auto &m : monsters)
m.y++;auto 自動幫你算出 m 的資料型態
查每一隻怪物,如果它超出邊界,就扣玩家的血 ,並刪除這隻怪物
monsters.erase(
remove_if(monsters.begin(), monsters.end(),[&](Monster m){
if (m.y >= r){ // 避免怪物跑出地圖
hp--; // 懲罰:扣玩家血量
return true; // 回傳這隻要刪掉
}
return false; // 回傳這隻留著
}),
monsters.end()
);函式名稱( 資料1 , 資料2 , 資料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);
}
偷偷在getch那邊加入一個控制:案按??鍵執行發射子彈的函式
char c = getch();
if (c=='a' && playerX > 1)playerX--;
else if (c=='d' && playerX < c)playerX++;
else if (c==' ')shoot();如果真的不習慣用switch,可以用if(人操作速度太慢對程式執行速度沒太大差距
當按空格時,會執行"shoot()"這個自訂義函式
偷偷在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>// 如果子彈往上
for (auto &b : bullets)//邏輯跟前面一樣
b.y--;依照慣性,那就會依直往前飛
但遊戲不一定要遵循物理,可以打破規則不走尋常路
Coco
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)? 你在跑迴圈的時候,如果突然把那一格抽掉 ,整個隊伍順序會亂掉,程式會當機。所以先發配邊疆!
monsters.erase(
remove_if(monsters.begin(), monsters.end(), [](Monster m){
return m.y > r;
}),
monsters.end()
);如果在邊疆,回傳true,告訴劊子手執行
一路砍到最後
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]);
}
}耶一耶一耶一耶喔喔