标签 Ncurses 下的文章

使用 ncurses 的灵活性和强大功能在 Linux 上创建一个猜数字游戏。

 title=

在我的 上一篇文章,我简要介绍了使用 ncurses 库通过 C 语言编写文本模式交互式应用程序。使用 ncurses,我们可以控制文本在终端上的显示位置和方式。如果你通过阅读手册页探索 ncurses 库函数,你会发现显示文本有很多不同的方式,包括粗体文本、颜色、闪烁文本、窗口、边框、图形字符和其它功能,这些都可以使你的应用脱颖而出。

如果你想探索一个更高级的程序来演示其中一些有趣的功能,有一个简单的“猜数字”游戏,我已更新为使用 ncurses 编写的了。该程序在一个范围内选择一个随机数,然后要求用户进行重复猜测,直到他们猜到这个秘密数字。当用户进行猜测时,程序会告知他们猜测的数字是太低还是太高。

请注意,程序限定可能的数字范围是 0 到 7。将值保持在有限的个位数数字范围内,可以更轻松的使用 getch() 函数从用户读取单个数字。我还使用了 getrandom 内核系统调用来生成随机数,设定数字最大值为 7,以从 0 (二进制 0000)到 7 (二进制 0111)中选择一个随机数。

#include <curses.h>;
#include <string.h>;          /* for strlen */
#include <sys/random.h>;      /* for getrandom */

int
random0_7()
{
   int num;
   getrandom(&num, sizeof(int), GRND_NONBLOCK);
   return (num & 7); /* from 0000 to 0111 */
}

int
read_guess()
{
  int ch;

  do {
    ch = getch();
  } while ((ch < '0') || (ch > '7'));

  return (ch - '0'); /* turn into a number */
}

通过使用 ncurses,我们可以增加一些有趣的视觉体验。通过添加函数,我们可以在屏幕顶部显示重要的文本信息,在屏幕底部显示状态消息行:

void
print_header(const char *text)
{
  move(0, 0);
  clrtoeol();

  attron(A_BOLD);
  mvaddstr(0, (COLS / 2) - (strlen(text) / 2), text);
  attroff(A_BOLD);
  refresh();
}

void
print_status(const char *text)
{
  move(LINES - 1, 0);
  clrtoeol();
 
  attron(A_REVERSE);
  mvaddstr(LINES - 1, 0, text);
  attroff(A_REVERSE);
  refresh();
}

通过这些函数,我们就可以构建猜数字游戏的主要部分。首先,程序为 ncurses 设置终端,然后从 0 到 7 中选择一个随机数。显示数字刻度后,程序启动一个循环,询问用户的猜测。

当用户进行猜测时,程序会在屏幕上提供反馈。如果猜测太低,程序会在屏幕上的数字下方打印一个左方括号。如果猜测太高,程序会在屏幕上的数字下方打印一个右方括号。这有助于用户缩小他们的选择范围,直到他们猜出正确的数字。

int
main()
{
  int number, guess;

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

  number = random0_7();
  mvprintw(1, COLS - 1, "%d", number); /* debugging */

  print_header("Guess the number 0-7");

  mvaddstr(9, (COLS / 2) - 7, "0 1 2 3 4 5 6 7");

  print_status("Make a guess...");

  do {
    guess = read_guess();

    move(10, (COLS / 2) - 7 + (guess * 2));

    if (guess < number) {
      addch('[');
      print_status("Too low");
    }

    else if (guess > number) {
      addch(']');
      print_status("Too high");
    }

    else {
      addch('^');
    }
  } while (guess != number);

  print_header("That's right!");
  print_status("Press any key to quit");
  getch();

  endwin();

  return 0;
}

复制这个程序,自己尝试编译它。不要忘记你需要告诉 GCC 编译器链接到 ncurses 库:

$ gcc -o guess guess.c -lncurses

我留下了一个调试行,所以你可以看到屏幕右上角附近的秘密数字:

guess number game interface

图1:猜数字游戏。注意右上角的秘密数字。

开始使用 ncurses

该程序使用了 ncurses 的许多其它函数,你可以从这些函数开始。例如,print_header 函数在屏幕顶部居中以粗体文本打印消息,print_status 函数在屏幕左下角以反向文本打印消息。使用它来帮助你开始使用 ncurses 编程。


via: https://opensource.com/article/21/8/guess-number-game-ncurses-linux

作者:Jim Hall 选题:lujun9972 译者:perfiffer 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

使用 ncurses 在 Linux 屏幕上的特定位置放置文本,可以带来更友好的用户界面体验。

 title=

大多数的 Linux 实用程序仅仅只在屏幕的底部滚动文本。如果你想在屏幕中放置你的文本,例如一个游戏或者一个数据展示,你可以试试 ncurses。

curses 是一个旧的 Unix 库,它可以在文本终端界面控制光标。curses 的名称就来自于术语 “ 光标控制 cursor control ”。多年以后,其他人编写了新的 curses 版本用来添加新的功能,新版本被叫做 “new curses” 或者 “ncurses”。你可以在每个流行的 Linux 发行版中找到 ncurses。尽管默认情况下可能未安装开发库、头文件和文档。例如,在 Fedora 上,你需要使用以下命令安装 ncurses-devel 包:

$ sudo dnf install ncurses-devel

在程序中使用 ncurses

要在屏幕上直接寻址,你首先需要初始化 ncurses 库。大部分程序会通过以下三行来做到这一点:

  • initscr():初始化窗口对象和 ncurses 代码,返回代表整个屏幕的窗口对象
  • cbreak():禁用缓冲并使键入的输入立即可用
  • noecho():关闭回显,因此用户输入不会显示在屏幕上

这些函数定义在 curses.h 头文件中,你需要在你的程序中通过以下方式将其包含进来:

#include <curses.h>

初始化终端后,你可以自由使用任何 ncurses 函数,我们将在示例程序中探讨其中的一些函数。

当你使用完 ncurses 并想返回到常规终端模式下时,使用 endwin() 重置一切。此命令可以重置任何屏幕颜色,将光标移动到屏幕的左下角,并使光标可见。通常在退出程序之前执行此操作。

在屏幕上寻址

关于 ncurses 首先需要知道的是屏幕的坐标分为行和列,左上角的是 0,0 点。ncurses 定义了两个全局变量来帮助你识别屏幕:LINES 是屏幕的行数,COLS 是屏幕的列数。屏幕右下角的位置是 LINES-1,COLS-1

例如,如果你想要移动光标到第 10 行和第 30 列,你可以使用 move() 函数,移动到此坐标:

move(10, 30);

之后显示的任何文本都将从屏幕的该位置开始。要显示单个字符,请对单个字符使用 addch(c) 函数。要显示字符串,将对字符串使用 addstr(s) 函数。对于类似于 printf 的格式化输出,请使用带有常用选项的 printw(fmt, ...)

移动到屏幕指定位置和显示文本是一件很常见的事情,ncurses 提供了同时执行这两项操作的快捷方式。mvaddch(row, col, c) 函数将在屏幕第 row 行,第 col 列的位置显示一个字符。而 mvaddstr(row, col, s) 函数将在屏幕第 row 行,第 col 列的位置显示一个字符串。举个更直接的例子,在程序中使用 mvaddstr(10, 30, "Welcome to ncurses"); 函数将从屏幕的第 10 行和第 30 列开始显示文本 Welcome to ncurses。使用 mvaddch(0, 0, '+') 函数将在屏幕的左上角第 0 行和第 0 列处显示一个加号(+)。

在终端屏幕上绘制文本会对某些系统产生性能影响,尤其是在较旧的硬件终端上。因此 ncurses 允许你“堆叠”一堆文本以显示在屏幕上,然后使用 refresh() 函数使所有这些更改对用户可见。

让我们来看一个将以上所有内容整合在一起的简单示例:

#include <curses.h>

int
main()
{
  initscr();
  cbreak();
  noecho();

  mvaddch(0, 0, '+');
  mvaddch(LINES - 1, 0, '-');
  mvaddstr(10, 30, "press any key to quit");
  refresh();

  getch();

  endwin();
}

程序的开始初始化了一个终端窗口,然后在屏幕的左上角打印了一个加号,在左下角打印了一个减号,在第 10 行和第 30 列打印了 press any key to quit 文本。程序通过使用 getch() 函数接收了键盘输入的单个字符,接着,使用 endwin() 函数在程序完全退出前重置了终端。

getch() 是一个很有用的函数,你可以使用它来做很多事情。我经常使用它在我退出程序前用来暂停。与大多数 ncurses 函数一样,还有一个名为 mvgetch(row, col)getch() 版本,用于在等待字符输入之前移动到屏幕位置的第 row 行,第 col 列。

使用 ncurses 编译

如果你尝试以通常的方式编译该示例程序,例如 gcc pause.c,你可能会从链接器中获得大量错误列表。那是因为 GNU C 编译器不会自动链接 ncurses 库。相反,你需要使用 -l ncurses 命令行选项加载它以进行链接。

$ gcc -o pause pause.c -lncurses

运行新程序将打印一条简单的 press any key to quit消息,该消息差不多位于屏幕中央:

centered message in a program window

图 1:程序中居中的 “press any key to quit” 消息。

使用 ncurses 构建更好的程序

探索 ncurses 库函数以了解在屏幕上显示文本的其它方法。你可以在 ncurses 的手册页中找到所有 ncurses 函数的列表。这给出了 ncurses 的一般概述,并提供了不同 ncurses 函数的类似表格的列表,并参考了包含完整详细信息的手册页。例如,在 curs_printw(3X) 手册页中描述了 printw,可以通过以下方式查看:

$ man 3x curs_printw

更简单点:

$ man curs_printw

使用 ncurses,你可以创建更多有趣的程序。通过在屏幕上的特定位置打印文本,你可以创建在终端中运行的游戏和高级实用程序。


via: https://opensource.com/article/21/8/ncurses-linux

作者:Jim Hall 选题:lujun9972 译者:perfiffer 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

GNU Ncurses 是一个允许用户编写基于文本的用户界面(TUI)的编程库。许多基于文本的游戏都是使用这个库创建的。一个受欢迎的例子是 PacVim,这是一款学习 VIM 命令的 CLI 游戏。在这篇简要的指南中,我将解释如何在类 Unix 操作系统中安装 Ncurses 库。

在 Linux 中安装 Ncurses 库

Ncurses 在大多数 Linux 发行版的默认仓库中都有。例如,你可以使用以下命令将其安装在基于 Arch 的系统上:

$ sudo pacman -S ncurses

在RHEL、CentOS 上:

$ sudo yum install ncurses-devel

在 Fedora 22 和更新版本上:

$ sudo dnf install ncurses-devel

在 Debian、Ubuntu、Linux Mint 上:

$ sudo apt-get install libncurses5-dev libncursesw5-dev

默认仓库中的 GNU ncureses 可能有点老了。如果你想要最新的稳定版本,可以从源代码进行编译和安装,如下所示。

这里下载最新的 ncurses 版本。在写这篇文章时,最新版本是 6.1。

$ wget https://ftp.gnu.org/pub/gnu/ncurses/ncurses-6.1.tar.gz

解压 tar 文件:

$ tar xzf ncurses-6.1.tar.gz

这将在当前目录中创建一个名为 ncurses-6.1 的文件夹。cd 到该目录:

$ cd ncurses-6.1
$ ./configure --prefix=/opt/ncurses

最后,使用以下命令进行编译和安装:

$ make
$ sudo make install

使用命令验证安装:

$ ls -la /opt/ncurses

就是这样。Ncurses 已经安装在 Linux 发行版上。继续使用 Ncurses 创建漂亮的 TUI。

还会有更多的好东西。敬请关注!

干杯!


via: https://www.ostechnix.com/how-to-install-ncurses-library-in-linux/

作者:SK 选题:lujun9972 译者:geekpi 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

Jim 给他的终端冒险游戏添加了颜色,演示了如何用 curses 操纵颜色。

在我的使用 ncurses 库进行编程的系列文章的第一篇第二篇中,我已经介绍了一些 curses 函数来在屏幕上作画、从屏幕上查询和从键盘读取字符。为了搞清楚这些函数,我使用 curses 来利用简单字符绘制游戏地图和玩家角色,创建了一个简单的冒险游戏。在这篇紧接着的文章里,我展示了如何为你的 curses 程序添加颜色。

在屏幕上绘图一切都挺好的,但是如果只有黑底白字的文本,你的程序可能看起来很无趣。颜色可以帮助传递更多的信息。举个例子,如果你的程序需要报告执行成功或者执行失败时。在这样的情况下你可以使用绿色或者红色来帮助强调输出。或者,你只是简单地想要“潮艺”一下给你的程序来让它看起来更美观。

在这篇文章中,我用一个简单的例子来展示通过 curses 函数进行颜色操作。在我先前的文章中,我写了一个可以让你在一个粗糙绘制的地图上移动玩家角色的初级冒险类游戏。但是那里面的地图完全是白色和黑色的文本,通过形状来表明是水()或者山(^)。所以,让我们将游戏更新到使用颜色的版本吧。

颜色要素

在你可以使用颜色之前,你的程序需要知道它是否可以依靠终端正确地显示颜色。在现代操作系统上,此处应该永远为true。但是在经典的计算机上,一些终端是单色的,例如古老的 VT52 和 VT100 终端,一般它们提供黑底白色或者黑底绿色的文本。

可以使用 has_colors() 函数查询终端的颜色功能。这个函数将会在终端可以显示颜色的时候返回 true,否则将会返回 false。这个函数一般用于 if 块的开头,就像这样:

if (has_colors() == FALSE) {
    endwin();
    printf("Your terminal does not support color\n");
    exit(1);
}

在知道终端可以显示颜色之后,你可以使用 start_color() 函数来设置 curses 使用颜色。现在是时候定义程序将要使用的颜色了。

在 curses 中,你应该按对定义颜色:一个前景色放在一个背景色上。这样允许 curses 一次性设置两个颜色属性,这也是一般你想要使用的方式。通过 init_pair() 函数可以定义一个前景色和背景色并关联到索引数字来设置颜色对。大致语法如下:

init_pair(index, foreground, background);

控制台支持八种基础的颜色:黑色、红色、绿色、黄色、蓝色、品红色、青色和白色。这些颜色通过下面的名称为你定义好了:

  • COLOR_BLACK
  • COLOR_RED
  • COLOR_GREEN
  • COLOR_YELLOW
  • COLOR_BLUE
  • COLOR_MAGENTA
  • COLOR_CYAN
  • COLOR_WHITE

应用颜色

在我的冒险游戏中,我想要让草地呈现绿色而玩家的足迹变成不易察觉的绿底黄色点迹。水应该是蓝色,那些表示波浪的 ~ 符号应该是近似青色的。我想让山(^)是灰色的,但是我可以用白底黑色文本做一个可用的折中方案。(LCTT 译注:意为终端预设的颜色没有灰色,使用白底黑色文本做一个折中方案)为了让玩家的角色更易见,我想要使用一个刺目的品红底红色设计。我可以像这样定义这些颜色对:

start_color();
init_pair(1, COLOR_YELLOW, COLOR_GREEN);
init_pair(2, COLOR_CYAN, COLOR_BLUE);
init_pair(3, COLOR_BLACK, COLOR_WHITE);
init_pair(4, COLOR_RED, COLOR_MAGENTA);

为了让颜色对更容易记忆,我的程序中定义了一些符号常量:

#define GRASS_PAIR     1
#define EMPTY_PAIR     1
#define WATER_PAIR     2
#define MOUNTAIN_PAIR  3
#define PLAYER_PAIR    4

有了这些常量,我的颜色定义就变成了:

start_color();
init_pair(GRASS_PAIR, COLOR_YELLOW, COLOR_GREEN);
init_pair(WATER_PAIR, COLOR_CYAN, COLOR_BLUE);
init_pair(MOUNTAIN_PAIR, COLOR_BLACK, COLOR_WHITE);
init_pair(PLAYER_PAIR, COLOR_RED, COLOR_MAGENTA);

在任何时候你想要使用颜色显示文本,你只需要告诉 curses 设置哪种颜色属性。为了更好的编程实践,你同样应该在你完成了颜色使用的时候告诉 curses 取消颜色组合。为了设置颜色,应该在调用像 mvaddch() 这样的函数之前使用attron(),然后通过 attroff() 关闭颜色属性。例如,在我绘制玩家角色的时候,我应该这样做:

attron(COLOR_PAIR(PLAYER_PAIR));
mvaddch(y, x, PLAYER);
attroff(COLOR_PAIR(PLAYER_PAIR));

记住将颜色应用到你的程序对你如何查询屏幕有一些微妙的影响。一般来讲,由 mvinch() 函数返回的值是没有带颜色属性的类型 chtype,这个值基本上是一个整型值,也可以当作整型值来用。但是,由于使用颜色添加了额外的属性到屏幕上的字符上,所以 chtype 按照扩展的位模式携带了额外的颜色信息。一旦你使用 mvinch(),返回值将会包含这些额外的颜色值。为了只提取文本值,例如在 is_move_okay() 函数中,你需要和 A_CHARTEXT& 位运算:

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 & A_CHARTEXT) == GRASS)
            || ((testch & A_CHARTEXT) == EMPTY));
}

通过这些修改,我可以用颜色更新这个冒险游戏:

/* quest.c */

#include <curses.h>
#include <stdlib.h>

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

#define GRASS_PAIR     1
#define EMPTY_PAIR     1
#define WATER_PAIR     2
#define MOUNTAIN_PAIR  3
#define PLAYER_PAIR    4

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();

    /* 初始化颜色 */

    if (has_colors() == FALSE) {
        endwin();
        printf("Your terminal does not support color\n");
        exit(1);
    }

    start_color();
    init_pair(GRASS_PAIR, COLOR_YELLOW, COLOR_GREEN);
    init_pair(WATER_PAIR, COLOR_CYAN, COLOR_BLUE);
    init_pair(MOUNTAIN_PAIR, COLOR_BLACK, COLOR_WHITE);
    init_pair(PLAYER_PAIR, COLOR_RED, COLOR_MAGENTA);

    clear();

    /* 初始化探索地图 */

    draw_map();

    /* 在左下角创建新角色 */

    y = LINES - 1;
    x = 0;

    do {

        /* 默认情况下,你获得了一个闪烁的光标--用来指明玩家 * */

        attron(COLOR_PAIR(PLAYER_PAIR));
        mvaddch(y, x, PLAYER);
        attroff(COLOR_PAIR(PLAYER_PAIR));
        move(y, x);
        refresh();

        ch = getch();

        /* 测试输入键值并获取方向 */

        switch (ch) {
        case KEY_UP:
        case 'w':
        case 'W':
            if ((y > 0) && is_move_okay(y - 1, x)) {
                attron(COLOR_PAIR(EMPTY_PAIR));
                mvaddch(y, x, EMPTY);
                attroff(COLOR_PAIR(EMPTY_PAIR));
                y = y - 1;
            }
            break;
        case KEY_DOWN:
        case 's':
        case 'S':
            if ((y < LINES - 1) && is_move_okay(y + 1, x)) {
                attron(COLOR_PAIR(EMPTY_PAIR));
                mvaddch(y, x, EMPTY);
                attroff(COLOR_PAIR(EMPTY_PAIR));
                y = y + 1;
            }
            break;
        case KEY_LEFT:
        case 'a':
        case 'A':
            if ((x > 0) && is_move_okay(y, x - 1)) {
                attron(COLOR_PAIR(EMPTY_PAIR));
                mvaddch(y, x, EMPTY);
                attroff(COLOR_PAIR(EMPTY_PAIR));
                x = x - 1;
            }
            break;
        case KEY_RIGHT:
        case 'd':
        case 'D':
            if ((x < COLS - 1) && is_move_okay(y, x + 1)) {
                attron(COLOR_PAIR(EMPTY_PAIR));
                mvaddch(y, x, EMPTY);
                attroff(COLOR_PAIR(EMPTY_PAIR));
                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 & A_CHARTEXT) == GRASS)
            || ((testch & A_CHARTEXT) == EMPTY));
}

void draw_map(void)
{
    int y, x;

    /* 绘制探索地图 */

    /* 背景 */

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

    /* 山峰和山路 */

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

    attron(COLOR_PAIR(GRASS_PAIR));
    mvhline(LINES / 4, 0, GRASS, COLS);
    attroff(COLOR_PAIR(GRASS_PAIR));

    /* 湖 */

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

你可能不能认出所有为了在冒险游戏里面支持颜色需要的修改,除非你目光敏锐。diff 工具展示了所有为了支持颜色而添加的函数或者修改的代码:

$ diff quest-color/quest.c quest/quest.c
12,17d11
< #define GRASS_PAIR     1
< #define EMPTY_PAIR     1
< #define WATER_PAIR     2
< #define MOUNTAIN_PAIR  3
< #define PLAYER_PAIR    4
<
33,46d26
<     /* initialize colors */
<
<     if (has_colors() == FALSE) {
<    endwin();
<    printf("Your terminal does not support color\n");
<    exit(1);
<     }
<
<     start_color();
<     init_pair(GRASS_PAIR, COLOR_YELLOW, COLOR_GREEN);
<     init_pair(WATER_PAIR, COLOR_CYAN, COLOR_BLUE);
<     init_pair(MOUNTAIN_PAIR, COLOR_BLACK, COLOR_WHITE);
<     init_pair(PLAYER_PAIR, COLOR_RED, COLOR_MAGENTA);
<
61d40
<    attron(COLOR_PAIR(PLAYER_PAIR));
63d41
<    attroff(COLOR_PAIR(PLAYER_PAIR));
76d53
<            attron(COLOR_PAIR(EMPTY_PAIR));
78d54
<            attroff(COLOR_PAIR(EMPTY_PAIR));
86d61
<            attron(COLOR_PAIR(EMPTY_PAIR));
88d62
<            attroff(COLOR_PAIR(EMPTY_PAIR));
96d69
<            attron(COLOR_PAIR(EMPTY_PAIR));
98d70
<            attroff(COLOR_PAIR(EMPTY_PAIR));
106d77
<            attron(COLOR_PAIR(EMPTY_PAIR));
108d78
<            attroff(COLOR_PAIR(EMPTY_PAIR));
128,129c98
<     return (((testch & A_CHARTEXT) == GRASS)
<        || ((testch & A_CHARTEXT) == EMPTY));
---
>     return ((testch == GRASS) || (testch == EMPTY));
140d108
<     attron(COLOR_PAIR(GRASS_PAIR));
144d111
<     attroff(COLOR_PAIR(GRASS_PAIR));
148d114
<     attron(COLOR_PAIR(MOUNTAIN_PAIR));
152d117
<     attroff(COLOR_PAIR(MOUNTAIN_PAIR));
154d118
<     attron(COLOR_PAIR(GRASS_PAIR));
156d119
<     attroff(COLOR_PAIR(GRASS_PAIR));
160d122
<     attron(COLOR_PAIR(WATER_PAIR));
164d125
<     attroff(COLOR_PAIR(WATER_PAIR));

开始玩吧--现在有颜色了

程序现在有了更舒服的颜色设计了,更匹配原来的桌游地图,有绿色的地、蓝色的湖和壮观的灰色山峰。英雄穿着红色的制服十分夺目。

图 1. 一个简单的带湖和山的桌游地图

图 2. 玩家站在左下角

图 3. 玩家可以在游戏区域移动,比如围绕湖,通过山的通道到达未知的区域。

通过颜色,你可以更清楚地展示信息。这个例子使用颜色指出可游戏的区域(绿色)相对着不可通过的区域(蓝色或者灰色)。我希望你可以使用这个示例游戏作为你自己的程序的一个起点或者参照。这取决于你需要你的程序做什么,你可以通过 curses 做得更多。

在下一篇文章,我计划展示 ncurses 库的其它特性,比如怎样创建窗口和边框。同时,如果你对于学习 curses 有兴趣,我建议你去读位于 Linux 文档计划 的 Pradeep Padala 写的 NCURSES Programming HOWTO


via: http://www.linuxjournal.com/content/programming-color-ncurses

作者:Jim Hall 译者:leemeans 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

怎样使用 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环境并退出。

开始玩

当你运行游戏时,玩家的字符在左下角初始化。当玩家在游戏区域四处移动的时候,程序创建了“一串”点。这样可以展示玩家经过了的点,让玩家避免经过不必要的路径。

图 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

本文由 LCTT 原创编译,Linux中国 荣誉推出

怎样使用 curses 来绘制终端屏幕?

虽然图形界面非常酷,但是不是所有的程序都需要点击式的界面。例如,令人尊敬的 Vi 编辑器在第一个 GUI 出现之前在纯文本终端运行了很久。

Vi 编辑器是一个在“文本”模式下绘制的 面向屏幕 screen-oriented 程序的例子。它使用了一个叫 curses 的库。这个库提供了一系列的编程接口来操纵终端屏幕。curses 库产生于 BSD UNIX,但是 Linux 系统通过 ncurses 库提供这个功能。

要了解 ncurses “过去曾引起的风暴”,参见 [ncurses: Portable Screen-Handling for Linux, September 1, 1995, by Eric S. Raymond.]

使用 curses 创建程序实际上非常简单。在这个文章中,我展示了一个利用 curses 来在终端屏幕上绘图的示例程序。

谢尔宾斯基三角形

简单展示一些 curses 函数的一个方法是生成 谢尔宾斯基三角形 Sierpinski's Triangle 。如果你对生成谢尔宾斯基三角形的这种方法不熟悉的话,这里是一些产生谢尔宾斯基三角形的规则:

  1. 设置定义三角形的三个点。
  2. 随机选择任意的一个点 (x,y)

然后:

  1. 在三角形的顶点中随机选择一个点。
  2. 将新的 x,y 设置为先前的 x,y 和三角顶点的中间点。
  3. 重复(上述步骤)。

所以我按照这些指令写了这个程序,程序使用 curses 函数来向终端屏幕绘制谢尔宾斯基三角形:

/* triangle.c */

#include <curses.h>
#include <stdlib.h>

#include "getrandom_int.h"

#define ITERMAX 10000

int main(void)
{
  long iter;
  int yi, xi;
  int y[3], x[3];
  int index;
  int maxlines, maxcols;

  /* initialize curses */

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

  clear();

  /* initialize triangle */

  maxlines = LINES - 1;
  maxcols = COLS - 1;

  y[0] = 0;
  x[0] = 0;

  y[1] = maxlines;
  x[1] = maxcols / 2;

  y[2] = 0;
  x[2] = maxcols;

  mvaddch(y[0], x[0], '0');
  mvaddch(y[1], x[1], '1');
  mvaddch(y[2], x[2], '2');

  /* initialize yi,xi with random values */

  yi = getrandom_int() % maxlines;
  xi = getrandom_int() % maxcols;

  mvaddch(yi, xi, '.');

  /* iterate the triangle */

  for (iter = 0; iter < ITERMAX; iter++) {
      index = getrandom_int() % 3;

      yi = (yi + y[index]) / 2;
      xi = (xi + x[index]) / 2;

      mvaddch(yi, xi, '*');
      refresh();
  }

  /* done */

  mvaddstr(maxlines, 0, "Press any key to quit");

  refresh();

  getch();
  endwin();

  exit(0);
}

让我一边解释一边浏览这个程序。首先,getrandom_int() 函数是我对 Linux 系统调用 getrandom() 的包装器。它保证返回一个正整数(int)值。(LCTT 译注:getrandom() 系统调用按照字节返回随机值到一个变量中,值是随机的,不保证正负,使用 stdlib.hrandom() 函数可以达到同样的效果)另外,按照上面的规则,你应该能够辨认出初始化和迭代谢尔宾斯基三角形的代码。除此之外,我们来看看我用来在终端上绘制三角形的 curses 函数。

大多数 curses 程序以这四条指令开头。 initscr() 函数获取包括大小和特征在内的终端类型,并设置终端支持的 curses 环境。cbreak() 函数禁用行缓冲并设置 curses 每次只接受一个字符。noecho() 函数告诉 curses 不要把输入回显到屏幕上。而 clear() 函数清空了屏幕:

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

  clear();

之后程序设置了三个定义三角的顶点。注意这里使用的 LINESCOLS,它们是由 initscr() 来设置的。这些值告诉程序在终端的行数和列数。屏幕坐标从 0 开始,所以屏幕左上角是 00 列。屏幕右下角是 LINES - 1 行,COLS - 1 列。为了便于记忆,我的程序里把这些值分别设为了变量 maxlinesmaxcols

在屏幕上绘制文字的两个简单方法是 addch()addstr() 函数。也可以使用相关的 mvaddch()mvaddstr() 函数可以将字符放到一个特定的屏幕位置。我的程序在很多地方都用到了这些函数。首先程序绘制三个定义三角的点并标记为 '0''1''2'

  mvaddch(y[0], x[0], '0');
  mvaddch(y[1], x[1], '1');
  mvaddch(y[2], x[2], '2');

为了绘制任意的一个初始点,程序做了类似的一个调用:

  mvaddch(yi, xi, '.');

还有为了在谢尔宾斯基三角形递归中绘制连续的点:

      mvaddch(yi, xi, '*');

当程序完成之后,将会在屏幕左下角(在 maxlines 行,0 列)显示一个帮助信息:

  mvaddstr(maxlines, 0, "Press any key to quit");

注意 curses 在内存中维护了一个版本的屏幕显示,并且只有在你要求的时候才会更新这个屏幕,这很重要。特别是当你想要向屏幕显示大量的文字的时候,这样程序会有更好的性能表现。这是因为 curses 只能更新在上次更新之后改变的这部分屏幕。想要让 curses 更新终端屏幕,请使用 refresh() 函数。

在我的示例程序中,我选择在“绘制”每个谢尔宾斯基三角形中的连续点时更新屏幕。通过这样做,用户可以观察三角形中的每次迭代。(LCTT 译注:由于 CPU 太快,迭代过程执行就太快了,所以其实很难直接看到迭代过程)

在退出之前,我使用 getch() 函数等待用户按下一个键。然后我调用 endwin() 函数退出 curses 环境并返回终端程序到一般控制。

  getch();
  endwin();

编译和示例输出

现在你已经有了你的第一个 curses 示例程序,是时候编译运行它了。记住 Linux 操作系统通过 ncurses 库来实现 curses 功能,所以你需要在编译的时候通过 -lncurses来链接——例如:

$ ls
getrandom_int.c  getrandom_int.h  triangle.c

$ gcc -Wall -lncurses -o triangle triangle.c getrandom_int.c

(LCTT 译注:此处命令行有问题,-lncurses 选项在我的 Ubuntu 16.04 系统 + gcc 4.9.3 环境下,必须放在命令行最后,否则找不到库文件,链接时会出现未定义的引用。)

在标准的 80x24 终端运行这个 triangle 程序并没什么意思。在那样的分辨率下你不能看见谢尔宾斯基三角形的很多细节。如果你运行终端窗口并设置非常小的字体大小,你可以更加容易地看到谢尔宾斯基三角形的不规则性质。在我的系统上,输出如图 1。

图 1. triangle 程序的输出

虽然迭代具有随机性,但是每次谢尔宾斯基三角形的运行看起来都会很一致。唯一的不同是最初绘制到屏幕的一些点的位置不同。在这个例子中,你可以看到三角形开始的一个小圆点,在点 1 附近。看起来程序接下来选择了点 2,然后你可以看到在圆点和“2”之间的星号。并且看起来程序随机选择了点 2 作为下一个随机数,因为你可以看到在第一个星号和“2”之间的星号。从这里开始,就不能继续分辨三角形是怎样被画出来的了,因为所有的连续点都属于三角形区域。

开始学习 ncurses

这个程序是一个怎样使用 curses 函数绘制字符到屏幕的简单例子。按照你的程序的需要,你可以通过 curses 做得更多。在下一篇文章中,我将会展示怎样使用 curses 让用户和屏幕交互。如果你对于学习 curses 有兴趣,我建议你去读位于 Linux 文档计划 Linux Documentation Project 的 Pradeep Padala 写的 NCURSES Programming HOWTO

关于作者

Jim Hall 是一个自由及开源软件的倡议者,他最有名的工作是 FreeDOS 计划,也同样致力于开源软件的可用性。Jim 是在明尼苏达州的拉姆齐县的首席信息官。


via: http://www.linuxjournal.com/content/getting-started-ncurses

作者:Jim Hall 译者:leemeans 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出