iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 23
1

引言

昨天我們實作了玩家的移動
能讓玩家在超簡單的地圖直接走到終點XD
堪稱最簡單遊戲(X

所以今天我們來為遊戲增添點難度,
讓遊戲多了的要素,讓玩家找鑰匙來開門!

不過只有一種門也很無聊吧!
我們來設計三種門與三種鑰匙,玩家必須找到對應鑰匙才能開門~
這樣應該會比較豐富。

以下是今天的規格表:

項目 內容
地圖文字檔 level1.txt

大家下載下來後一樣把它放到maps資料夾裡,然後在LoadMap.h中加上:

#define LEVEL1_MAP "maps/level1.txt"

鑰匙數量與種類

鑰匙我們一共會設計三種,分別叫a, b, c鑰匙,
門也有三種,分別叫A, B, C門,就是以大小寫分開~

我們先設計鑰匙,因為鑰匙是玩家帶著的,我們在Player結構裡加上鑰匙欄位:

/* File: PlayGround.h */

typedef struct player
{
    char skin;
    int x;
    int y;
    
// ------------新增部分-------------------
    int key_a_num;  // 三個整數分別記錄a, b, c鑰匙的數量
    int key_b_num;  // 取用方式就是直接 p.key_a_num += 1; 之類的就行了
    int key_c_num;
// --------------------------------------
}Player;

有了鑰匙的定義,

初始化、初始化、初始化

因為很重要講三遍XD

就先初始化吧:

/* File: PlayGround.c */

void initPlayer(char skin, int x, int y)
{
    isPlaying = 1;
    p.skin = skin;
    p.x = x;
    p.y = y;
    
// ------------新增部分-------------------
    p.key_a_num = 0;  // 一開始都是0支
    p.key_b_num = 0;
    p.key_c_num = 0;
// --------------------------------------

    game_map[y][x] = skin;
}

狀態欄

喔喔,新的概念來了~

這會是為什麼要將遊戲視窗高度加10格的原因之一。

為了顯示玩家目前的狀態(攜帶的物品之類的),
我們需要有個地方可以展示各種遊戲數值目前是多少

那就是狀態欄啦,狀態欄地圖分開顯示,
大概如下圖所示:
https://ithelp.ithome.com.tw/upload/images/20191007/20111429CN5e8DduRx.png

那在實際程式顯示上呢,
會遵守以下順序:

  1. 地圖顯示(showMap)
  2. 狀態欄(playerStatus)
  3. (Optional) 遊戲訊息是機動性的,事件發生才會顯示,所以狀態欄底下會保留給遊戲訊息使用。

以下我們來實作狀態欄吧:

/* File: PlayGround.h */

// 寫在PlayGround函數之下

void playerStatus();

/* File: PlayGround.c */

// 寫在PlayGround函數之下

void playerStatus()
{
    printf("[道具欄] 鑰匙A: %02d支  鑰匙B: %02d支  鑰匙C: %02d支\n", p.key_a_num, p.key_b_num, p.key_c_num);  // 分別表示出a, b, c鑰匙的數量
}

定義好之後,在PlayGround函數的半無窮迴圈中中呼叫playerStatus,代表每一循環都更新一次狀態欄:

/* File: PlayGround.c */

// PlayGround函數中的半無窮迴圈

while(isPlaying)
{
    selfCls();
    showMap();
    
// ------------新增部分-------------------
    playerStatus();  // 接在showMap底下,印出狀態欄
// --------------------------------------

// ------------新增部分-------------------
    if(GetAsyncKeyState(VK_ESCAPE) != 0)  // 這邊我們順便新增一個按鍵,當Esc被按下時,離開遊戲
    {
        playMusic(SYSTEM_OK, SE_MODE);
        exit(0);
    }
// --------------------------------------

    if(GetAsyncKeyState(VK_RIGHT) != 0)
    {
        movePlayer(p.x + 1, p.y);
    }
    if(GetAsyncKeyState(VK_LEFT) != 0)
    {
        movePlayer(p.x - 1, p.y);
    }
    if(GetAsyncKeyState(VK_UP) != 0)
    {
        movePlayer(p.x, p.y - 1);
    }
    if(GetAsyncKeyState(VK_DOWN) != 0)
    {
        movePlayer(p.x, p.y + 1);
    }
}

等待玩家觀看結果 & 詢問玩家問題

在進入鑰匙與門的單元前,我們必須先定義兩個常用的功能:等待 以及 詢問
遊戲總是會有要等待玩家看目前結果的時刻,以及詢問玩家選擇的時刻,
我們把這兩個函數命名為:waitForUser, askUser。

/* File: PlayGround.h */

// 寫在playerStatus底下

void waitForUser(int t);  // 傳入一個時間t,代表等待時間
int askUser(char *question);  // 傳入將要問的問題,並回傳int型態的結果(是或否)

/* File: PlayGround.c */

// 寫在playerStatus底下

void waitForUser(int t)
{
    Sleep(t);  // Sleep函數定義在windows.h,系統會暫停t秒
    system("cls");  // 清空全畫面
}

int askUser(char *question)
{
    printf("%s\n(按下F1確定,F2取消)", question);  // 輸出傳入的問題外,
                                                  // 再換一行輸出"(按下F1確定,F2取消)"

    while(1)
    {
        if(GetAsyncKeyState(VK_F1) != 0)  // 按下F1,就表示"是",回傳1
        {
            return 1;
        }
        else if(GetAsyncKeyState(VK_F2) != 0)  // 按下F2,就表示"否",回傳0
        {
            return 0;
        }
    }
}


開門 — 地圖物件判斷

我們前面寫的movePlayer函數只判斷了兩種情況:空白 以及 出口
現在我們新增了 鑰匙 的要素,也就是地圖上會出現門與鑰匙,
這個我們必須把狀況加進movePlayer中。

/* File: PlayGround.c */

void movePlayer(int x, int y)
{
    if(game_map[y][x] == ' ')
    {
        game_map[p.y][p.x] = ' ';
        game_map[y][x] = p.skin;

        p.x = x;
        p.y = y;
    }
    
// ------------新增部分-------------------
    if(game_map[y][x] == 'a')  // 下一步如果是鑰匙a(撿到鑰匙a的情況)
    {
        game_map[p.y][p.x] = ' ';
        game_map[y][x] = p.skin;

        p.x = x;
        p.y = y;  // 到這邊為止,是讓玩家走到下一格

        printf("發現A鑰匙!\n");  // 提醒玩家撿到開A門的鑰匙了
        playMusic(SYSTEM_OK, SE_MODE);  // 播放音效
        waitForUser(500);  // 等待0.5秒並清空全畫面
        p.key_a_num += 1;  // 玩家擁有的a鑰匙加了1支
    }
    if(game_map[y][x] == 'b')
    {
        game_map[p.y][p.x] = ' ';
        game_map[y][x] = p.skin;

        p.x = x;
        p.y = y;

        printf("發現B鑰匙!\n");
        playMusic(SYSTEM_OK, SE_MODE);
        waitForUser(500);
        p.key_b_num += 1;
    }
    if(game_map[y][x] == 'c')
    {
        game_map[p.y][p.x] = ' ';
        game_map[y][x] = p.skin;

        p.x = x;
        p.y = y;

        printf("發現C鑰匙!\n");
        playMusic(SYSTEM_OK, SE_MODE);
        waitForUser(500);
        p.key_c_num += 1;
    }
    if(game_map[y][x] == 'A')  // 下一步如果是A門
    {
        if(p.key_a_num > 0)  // 先看玩家有沒有至少1支a鑰匙,沒有的話沒有任何反應
        {
            if(askUser("遇到A門,是否開啟?"))  // 如果有,詢問玩家是否要花費鑰匙開啟
            {
                game_map[p.y][p.x] = ' ';
                game_map[y][x] = p.skin;

                p.x = x;
                p.y = y;

                printf("使用鑰匙a開啟A門!\n");  // 提醒玩家已經開啟了
                playMusic(UNLOCK, SE_MODE);  // 播放解鎖音效
                waitForUser(500);  // 等待0.5秒並清空全畫面
                p.key_a_num -= 1;  // 玩家擁有的a鑰匙少了1支
            }
        }
    }
    if(game_map[y][x] == 'B')
    {
        if(p.key_b_num > 0)
        {
            if(askUser("遇到B門,是否開啟?"))
            {
                game_map[p.y][p.x] = ' ';
                game_map[y][x] = p.skin;

                p.x = x;
                p.y = y;

                printf("使用鑰匙b開啟B門!\n");
                playMusic(UNLOCK, SE_MODE);
                waitForUser(500);
                p.key_b_num -= 1;
            }
        }
    }
    if(game_map[y][x] == 'C')
    {
        if(p.key_c_num > 0)
        {
            if(askUser("遇到C門,是否開啟?"))
            {
                game_map[p.y][p.x] = ' ';
                game_map[y][x] = p.skin;

                p.x = x;
                p.y = y;

                printf("使用鑰匙c開啟C門!\n");
                playMusic(UNLOCK, SE_MODE);
                waitForUser(500);
                p.key_c_num -= 1;
            }
        }
    }
// --------------------------------------

    if(game_map[y][x] == 'E')
    {
        playMusic(VICTORY, SE_MODE);
        printf("抵達終點!\n");
        system("pause");
        isPlaying = 0;
    }
}

main函數呼叫

這部分其實也是很容易忘記,大家記得函數定義完後,要回到main函數做呼叫啊~

/* File: main.c */

#include "SystemSetting.h"  // 設定標題、視窗、音效、錯誤處理、遊戲初始化等
#include "LoadMap.h"  // 載入地圖、頁面
#include "PlayGround.h"  // 遊戲遊玩頁面、玩家設定

int main(int argc, char *argv[])
{
    playMusic(BGM, BGM_MODE);
    setGameTitle(GAME_TITLE);
    setGameWindow(GAME_WIDTH, GAME_HEIGHT);

    initMap();

    loadMap(TITLE_MAP);
    showMap();
    system("pause");
    playMusic(MUSIC_STOP, BGM_MODE);
    playMusic(SYSTEM_OK, SE_MODE);

    playGround(LEVEL0_MAP, 28, 4);
    
// ------------新增部分-------------------
    playGround(LEVEL1_MAP, 28, 27);  // 加了level1關卡
// --------------------------------------

    return 0;
}

如果在其他檔案把函數寫好了,在main函數就會像這樣非常乾淨,要加新的一關只要加一行code就好囉!因此也希望大家學習這種方式,初學者通常會把所有東西都放在單一個main函數裡,程式一大起來其實非常不好維護~

所以分檔寫很重要哦!


執行

都完成後,就可以執行啦!

https://ithelp.ithome.com.tw/upload/images/20191007/20111429KgX21e4CGT.png
第0關抵達終點後

https://ithelp.ithome.com.tw/upload/images/20191007/20111429j9dZJB7YYZ.png
就到達第1關啦!

https://ithelp.ithome.com.tw/upload/images/20191007/20111429MtkKTEyONv.png
撿到a鑰匙了~

https://ithelp.ithome.com.tw/upload/images/20191007/20111429MB2u2mVAi4.png
開A門,系統詢問要不要開啟?(這邊就是askUser的結果)

https://ithelp.ithome.com.tw/upload/images/20191007/20111429HjZ982Zs3W.png
鏗!門打開了!

大家可以過到終點試試看,筆者有設計過地圖,是可以玩的XD

明天我們再來加一些功能吧~


上一篇
[11屆鐵人賽Day22] 2D遊戲—人物控制
下一篇
[11屆鐵人賽Day24] 2D遊戲—生命、攻擊、防禦
系列文
若沒有遊戲引擎、合作夥伴...做得出遊戲嗎? 不試試看不知道吧? [使用C語言]30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
sixwings
iT邦研究生 4 級 ‧ 2019-10-08 17:23:18

沒有release程式,不能玩QQ

感謝留言~
30天會把整個程式release出來開放下載!

我要留言

立即登入留言