通過 ncurses 在終端建立一個冒險遊戲

Jim Hall發表於2018-02-25

怎樣使用 curses 函式讀取鍵盤並操作螢幕。

之前的文章介紹了 ncurses 庫,並提供了一個簡單的程式展示了一些將文字放到螢幕上的 curses 函式。在接下來的文章中,我將介紹如何使用其它的 curses 函式。

探險

當我逐漸長大,家裡有了一臺蘋果 II 電腦。我和我兄弟正是在這臺電腦上自學瞭如何用 AppleSoft BASIC 寫程式。我在寫了一些數學智力遊戲之後,繼續創造遊戲。作為 80 年代的人,我已經是龍與地下城桌遊的粉絲,在遊戲中角色扮演一個追求打敗怪物並在陌生土地上搶掠的戰士或者男巫,所以我建立一個基本的冒險遊戲也在情理之中。

AppleSoft BASIC 支援一種簡潔的特性:在標準解析度圖形模式(GR 模式)下,你可以檢測螢幕上特定點的顏色。這為建立一個冒險遊戲提供了捷徑。比起建立並更新週期性傳送到螢幕的記憶體地圖,我現在可以依賴 GR 模式為我維護地圖,我的程式還可以在玩家的角色(LCTT 譯註:此處 character 雙關一個代表玩家的角色,同時也是一個字元)在螢幕四處移動的時候查詢螢幕。通過這種方式,我讓電腦完成了大部分艱難的工作。因此,我的自頂向下的冒險遊戲使用了塊狀的 GR 模式圖形來展示我的遊戲地圖。

我的冒險遊戲使用了一張簡單的地圖,上面有一大片綠地伴著山脈從中間蔓延向下和一個在左上方的大湖。我要粗略地為桌遊戰役繪製這個地圖,其中包含一個允許玩家穿過到遠處的狹窄通道。

圖 1. 一個有湖和山的簡單桌遊地圖

你可以用 curses 繪製這個地圖,並用字元代表草地、山脈和水。接下來,我描述怎樣使用 curses 那樣做,以及如何在 Linux 終端建立和進行類似的一個冒險遊戲。

構建程式

在我的上一篇文章,我提到了大多數 curses 程式以相同的一組指令獲取終端型別和設定 curses 環境:

initscr();
cbreak();
noecho();

在這個程式,我新增了另外的語句:

keypad(stdscr, TRUE);

這裡的 TRUE 標誌允許 curses 從使用者終端讀取小鍵盤和功能鍵。如果你想要在你的程式中使用上下左右方向鍵,你需要使用這裡的 keypad(stdscr, TRUE)

這樣做了之後,你現在可以開始在終端螢幕上繪圖了。curses 函式包括了一系列在螢幕上繪製文字的方法。在我之前的文章中,我展示了 addch()addstr() 函式以及在新增文字之前先移動到指定螢幕位置的對應函式 mvaddch()mvaddstr()。為了在終端上建立這個冒險遊戲的地圖,你可以使用另外一組函式:vline()hline(),以及它們對應的函式 mvvline()mvhline()。這些 mv 函式接受螢幕座標、一個要繪製的字元和要重複此字元的次數的引數。例如,mvhline(1, 2, '-', 20) 將會繪製一條開始於第一行第二列並由 20 個橫線組成的線段。

為了以程式設計方式繪製地圖到終端螢幕上,讓我們先定義這個 draw_map() 函式:

#define GRASS     ' '
#define EMPTY     '.'
#define WATER     '~'
#define MOUNTAIN  '^'
#define PLAYER    '*'

void draw_map(void)
{
    int y, x;

    /* 繪製探索地圖 */

    /* 背景 */

    for (y = 0; y < LINES; y++) {
        mvhline(y, 0, GRASS, COLS);
    }

    /* 山和山道 */

    for (x = COLS / 2; x < COLS * 3 / 4; x++) {
        mvvline(0, x, MOUNTAIN, LINES);
    }

    mvhline(LINES / 4, 0, GRASS, COLS);

    /* 湖 */

    for (y = 1; y < LINES / 2; y++) {
        mvhline(y, 1, WATER, COLS / 3);
    }
}

在繪製這副地圖時,記住填充大塊字元到螢幕所使用的 mvvline()mvhline() 函式。我繪製從 0 列開始的字元水平線(mvhline)以建立草地區域,直到佔滿整個螢幕的高度和寬度。我繪製從 0 行開始的多條垂直線(mvvline)在此上新增了山脈,繪製單行水平線新增了一條山道(mvhline)。並且,我通過繪製一系列短水平線(mvhline)建立了湖。這種繪製重疊方塊的方式看起來似乎並沒有效率,但是記住在我們呼叫 refresh() 函式之前 curses 並不會真正更新螢幕。

繪製完地圖,建立遊戲就還剩下進入迴圈讓程式等待使用者按下上下左右方向鍵中的一個然後讓玩家圖示正確移動了。如果玩家想要移動的地方是空的,就應該允許玩家到那裡。

你可以把 curses 當做捷徑使用。比起在程式中例項化一個版本的地圖並複製到螢幕這麼複雜,你可以讓螢幕為你跟蹤所有東西。inch() 函式和相關聯的 mvinch() 函式允許你探測螢幕的內容。這讓你可以查詢 curses 以瞭解玩家想要移動到的位置是否被水填滿或者被山阻擋。這樣做你需要一個之後會用到的一個幫助函式:

int is_move_okay(int y, int x)
{
    int testch;

    /* 如果要進入的位置可以進入,返回 true */

    testch = mvinch(y, x);
    return ((testch == GRASS) || (testch == EMPTY));
}

如你所見,這個函式探測行 x、列 y 並在空間未被佔據的時候返回 true,否則返回 false

這樣我們寫移動迴圈就很容易了:從鍵盤獲取一個鍵值然後根據是上下左右鍵移動使用者字元。這裡是一個這種迴圈的簡單版本:


    do {
        ch = getch();

        /* 測試輸入的值並獲取方向 */

        switch (ch) {
        case KEY_UP:
            if ((y > 0) && is_move_okay(y - 1, x)) {
                y = y - 1;
            }
            break;
        case KEY_DOWN:
            if ((y < LINES - 1) && is_move_okay(y + 1, x)) {
                y = y + 1;
            }
            break;
        case KEY_LEFT:
            if ((x > 0) && is_move_okay(y, x - 1)) {
                x = x - 1;
            }
            break;
        case KEY_RIGHT
            if ((x < COLS - 1) && is_move_okay(y, x + 1)) {
                x = x + 1;
            }
            break;
        }
    }
    while (1);

為了在遊戲中使用這個迴圈,你需要在迴圈裡新增一些程式碼來啟用其它的鍵(例如傳統的移動鍵 WASD),以提供讓使用者退出遊戲和在螢幕上四處移動的方法。這裡是完整的程式:

/* quest.c */

#include 
#include 

#define GRASS     ' '
#define EMPTY     '.'
#define WATER     '~'
#define MOUNTAIN  '^'
#define PLAYER    '*'

int is_move_okay(int y, int x);
void draw_map(void);

int main(void)
{
    int y, x;
    int ch;

    /* 初始化curses */

    initscr();
    keypad(stdscr, TRUE);
    cbreak();
    noecho();

    clear();

    /* 初始化探索地圖 */

    draw_map();

    /* 在左下角初始化玩家 */

    y = LINES - 1;
    x = 0;

    do {
    /* 預設獲得一個閃爍的游標--表示玩家字元 */

    mvaddch(y, x, PLAYER);
    move(y, x);
    refresh();

    ch = getch();

    /* 測試輸入的鍵並獲取方向 */

    switch (ch) {
    case KEY_UP:
    case 'w':
    case 'W':
        if ((y > 0) && is_move_okay(y - 1, x)) {
        mvaddch(y, x, EMPTY);
        y = y - 1;
        }
        break;
    case KEY_DOWN:
    case 's':
    case 'S':
        if ((y < LINES - 1) && is_move_okay(y + 1, x)) {
        mvaddch(y, x, EMPTY);
        y = y + 1;
        }
        break;
    case KEY_LEFT:
    case 'a':
    case 'A':
        if ((x > 0) && is_move_okay(y, x - 1)) {
        mvaddch(y, x, EMPTY);
        x = x - 1;
        }
        break;
    case KEY_RIGHT:
    case 'd':
    case 'D':
        if ((x < COLS - 1) && is_move_okay(y, x + 1)) {
        mvaddch(y, x, EMPTY);
        x = x + 1;
        }
        break;
    }
    }
    while ((ch != 'q') && (ch != 'Q'));

    endwin();

    exit(0);
}

int is_move_okay(int y, int x)
{
    int testch;

    /* 當空間可以進入時返回true */

    testch = mvinch(y, x);
    return ((testch == GRASS) || (testch == EMPTY));
}

void draw_map(void)
{
    int y, x;

    /* 繪製探索地圖 */

    /* 背景 */

    for (y = 0; y < LINES; y++) {
    mvhline(y, 0, GRASS, COLS);
    }

    /* 山脈和山道 */

    for (x = COLS / 2; x < COLS * 3 / 4; x++) {
    mvvline(0, x, MOUNTAIN, LINES);
    }

    mvhline(LINES / 4, 0, GRASS, COLS);

    /* 湖 */

    for (y = 1; y < LINES / 2; y++) {
    mvhline(y, 1, WATER, COLS / 3);
    }
}

在完整的程式清單中,你可以看見使用 curses 函式建立遊戲的完整佈置:

  1. 初始化 curses 環境。
  2. 繪製地圖。
  3. 初始化玩家座標(左下角)
  4. 迴圈:
    • 繪製玩家的角色。
    • 從鍵盤獲取鍵值。
    • 對應地上下左右調整玩家座標。
    • 重複。
  5. 完成時關閉curses環境並退出。

開始玩

當你執行遊戲時,玩家的字元在左下角初始化。當玩家在遊戲區域四處移動的時候,程式建立了“一串”點。這樣可以展示玩家經過了的點,讓玩家避免經過不必要的路徑。

通過 ncurses 在終端建立一個冒險遊戲

圖 2. 初始化在左下角的玩家

通過 ncurses 在終端建立一個冒險遊戲

圖 3. 玩家可以在遊戲區域四處移動,例如湖周圍和山的通道

為了建立上面這樣的完整冒險遊戲,你可能需要在他/她的角色在遊戲區域四處移動的時候隨機建立不同的怪物。你也可以建立玩家可以發現在打敗敵人後可以掠奪的特殊道具,這些道具應能提高玩家的能力。

但是作為起點,這是一個展示如何使用 curses 函式讀取鍵盤和操縱螢幕的好程式。

下一步

這是一個如何使用 curses 函式更新和讀取螢幕和鍵盤的簡單例子。按照你的程式需要做什麼,curses 可以做得更多。在下一篇文章中,我計劃展示如何更新這個簡單程式以使用顏色。同時,如果你想要學習更多 curses,我鼓勵你去讀位於 Linux 文件計劃的 Pradeep Padala 寫的如何使用 NCURSES 程式設計


via: http://www.linuxjournal.com/content/creating-adventure-game-terminal-ncurses

作者:Jim Hall 譯者:Leemeans 校對:wxy

本文由 LCTT 原創編譯,Linux中國 榮譽推出

相關文章