通過 ncurses 在終端創建一個冒險遊戲
怎樣使用 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 函數創建遊戲的完整布置:
- 初始化 curses 環境。
- 繪製地圖。
- 初始化玩家坐標(左下角)
- 循環:
- 繪製玩家的角色。
- 從鍵盤獲取鍵值。
- 對應地上下左右調整玩家坐標。
- 重複。
- 完成時關閉curses環境並退出。
開始玩
當你運行遊戲時,玩家的字元在左下角初始化。當玩家在遊戲區域四處移動的時候,程序創建了「一串」點。這樣可以展示玩家經過了的點,讓玩家避免經過不必要的路徑。
圖 2. 初始化在左下角的玩家
圖 3. 玩家可以在遊戲區域四處移動,例如湖周圍和山的通道
為了創建上面這樣的完整冒險遊戲,你可能需要在他/她的角色在遊戲區域四處移動的時候隨機創建不同的怪物。你也可以創建玩家可以發現在打敗敵人後可以掠奪的特殊道具,這些道具應能提高玩家的能力。
但是作為起點,這是一個展示如何使用 curses 函數讀取鍵盤和操縱屏幕的好程序。
下一步
這是一個如何使用 curses 函數更新和讀取屏幕和鍵盤的簡單例子。按照你的程序需要做什麼,curses 可以做得更多。在下一篇文章中,我計劃展示如何更新這個簡單程序以使用顏色。同時,如果你想要學習更多 curses,我鼓勵你去讀位於 Linux 文檔計劃的 Pradeep Padala 寫的如何使用 NCURSES 編程。
via: http://www.linuxjournal.com/content/creating-adventure-game-terminal-ncurses
作者:Jim Hall 譯者:Leemeans 校對:wxy
本文轉載來自 Linux 中國: https://github.com/Linux-CN/archive