使用 ncurses 在终端中创建冒险游戏
如何使用 curses
函数读取键盘并操作屏幕。
我的上一篇文章介绍了 ncurses
库,并提供了一个简单的程序,演示了一些 curses
函数来在屏幕上显示文本。在这篇后续文章中,我将说明如何使用其他一些 curses
函数。
在我成长的过程中,我的家里有一台 Apple II 电脑。我和我的兄弟在这台机器上自学了如何用 AppleSoft BASIC 编写程序。在编写了一些数学谜题后,我开始创建游戏。由于在 20 世纪 80 年代长大,我已经成为了龙与地下城桌面游戏的粉丝,在游戏中,你扮演战士或巫师,执行任务来击败怪物并在陌生的土地上掠夺战利品。因此,我创建了一个简陋的冒险游戏也就不足为奇了。
AppleSoft BASIC 编程环境支持一个很棒的功能:在标准分辨率图形模式(GR 模式)下,您可以探测屏幕上特定像素的颜色。这为创建冒险游戏提供了一个捷径。我没有创建和更新定期传输到屏幕的内存地图,而是可以依靠 GR 模式为我维护地图,并且我的程序可以在玩家角色在屏幕上移动时查询屏幕。使用这种方法,我让电脑完成了大部分繁重的工作。因此,我的自上而下的冒险游戏使用块状 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 个破折号组成的水平线,从第 1 行、第 2 列开始。
为了以编程方式将地图绘制到终端屏幕,让我们定义这个 draw_map()
函数
#define GRASS ' '
#define EMPTY '.'
#define WATER '~'
#define MOUNTAIN '^'
#define PLAYER '*'
void draw_map(void)
{
int y, x;
/* draw the quest map */
/* background */
for (y = 0; y < LINES; y++) {
mvhline(y, 0, GRASS, COLS);
}
/* mountains, and mountain path */
for (x = COLS / 2; x < COLS * 3 / 4; x++) {
mvvline(0, x, MOUNTAIN, LINES);
}
mvhline(LINES / 4, 0, GRASS, COLS);
/* lake */
for (y = 1; y < LINES / 2; y++) {
mvhline(y, 1, WATER, COLS / 3);
}
}
在绘制此地图时,请注意使用 mvvline()
和 mvhline()
在屏幕上填充大块字符。我通过绘制从第 0 列开始的水平线 (mvhline
) 创建了草地,覆盖了屏幕的整个高度和宽度。我在其顶部添加了山脉,方法是绘制垂直线 (mvvline
),从第 0 行开始,并通过绘制单条水平线 (mvhline
) 创建了一条山路。并且,我通过绘制一系列短水平线 (mvhline
) 创建了湖泊。以这种方式绘制重叠的矩形似乎效率低下,但请记住,在稍后调用 refresh()
函数之前,curses
实际上不会更新屏幕。
绘制地图后,创建游戏剩下的工作就是进入一个循环,程序在其中等待用户按下向上、向下、向左或向右方向键之一,然后适当地移动玩家图标。如果玩家想要移动到的空间未被占用,则允许玩家进入那里。
您可以将 curses
用作快捷方式。您无需在程序中实例化地图版本并将此地图复制到屏幕,而是可以让屏幕为您跟踪一切。 inch()
函数和相关的 mvinch()
函数允许您探测屏幕的内容。这允许您查询 curses
以找出玩家想要移动到的空间是否已经被水填充或被山脉阻挡。为此,您需要一个稍后将使用的辅助函数
int is_move_okay(int y, int x)
{
int testch;
/* return true if the space is okay to move into */
testch = mvinch(y, x);
return ((testch == GRASS) || (testch == EMPTY));
}
如您所见,此函数探测列 y、行 x 处的位置,如果空间适当未被占用,则返回 true,否则返回 false。
这使得编写导航循环变得非常容易:从键盘获取一个键,并根据向上、向下、向左和向右箭头键移动用户的角色。这是一个简化的循环版本
do {
ch = getch();
/* test inputted key and determine direction */
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 <curses.h>
#include <stdlib.h>
#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;
/* initialize curses */
initscr();
keypad(stdscr, TRUE);
cbreak();
noecho();
clear();
/* initialize the quest map */
draw_map();
/* start player at lower-left */
y = LINES - 1;
x = 0;
do {
/* by default, you get a blinking cursor - use it to indicate player */
mvaddch(y, x, PLAYER);
move(y, x);
refresh();
ch = getch();
/* test inputted key and determine direction */
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;
/* return true if the space is okay to move into */
testch = mvinch(y, x);
return ((testch == GRASS) || (testch == EMPTY));
}
void draw_map(void)
{
int y, x;
/* draw the quest map */
/* background */
for (y = 0; y < LINES; y++) {
mvhline(y, 0, GRASS, COLS);
}
/* mountains, and mountain path */
for (x = COLS / 2; x < COLS * 3 / 4; x++) {
mvvline(0, x, MOUNTAIN, LINES);
}
mvhline(LINES / 4, 0, GRASS, COLS);
/* lake */
for (y = 1; y < LINES / 2; y++) {
mvhline(y, 1, WATER, COLS / 3);
}
}
在完整的程序列表中,您可以看到用于创建游戏的完整 curses
函数排列
1) 初始化 curses
环境。
2) 绘制地图。
3) 初始化玩家坐标(左下角)。
4) 循环
- 绘制玩家的角色。
- 从键盘获取一个键。
- 相应地向上、向下、向左或向右调整玩家的坐标。
- 重复。
5) 完成后,关闭 curses
环境并退出。
当您运行游戏时,玩家的角色从左下角开始。当玩家在游戏区域移动时,程序会创建一个“轨迹”点。这有助于显示玩家之前去过哪里,以便玩家可以避免不必要地穿过路径。

图 2. 玩家在左下角开始游戏。

图 3. 玩家可以在游戏区域内移动,例如绕湖和穿过山口。
为了在此基础上创建一个完整的冒险游戏,您可以在玩家在游戏区域内导航其角色时添加与各种怪物的随机遭遇。您还可以包括玩家在击败敌人后可以发现或掠夺的特殊物品,这将进一步增强玩家的能力。
但首先,这是一个很好的程序,用于演示如何使用 curses
函数读取键盘和操作屏幕。
此程序是如何使用 curses
函数更新和读取屏幕和键盘的简单示例。根据您的程序需要执行的操作,您可以使用 curses
做更多的事情。在后续文章中,我计划展示如何更新此示例程序以使用颜色。同时,如果您有兴趣了解更多关于 curses
的信息,我建议您阅读 Pradeep Padala 在 Linux 文档项目中的 NCURSES Programming HOWTO。