编写控制台游戏程序

现在让我们使用所学的知识完成一个游戏程序。这里我们将不使用任何图形界面,而是制作一个简单的、控制台上运行的字符界面的游戏。

需要注意Windows的控制台程序和Linux的控制台程序需要使用各自不同的方法来实现诸如光标移动,颜色设置等操作,下面分别讲解。

清屏

在Windows下控制台窗口的控制是基于win32 api, 就是那些在cmd下可以执行的命令, 使用之前需要引入头文件windows.h

例如Windows下清除屏幕:

system("cls");

而在Linux下是通过Shell命令,只需要引入stdlib.h即可,比如要实现清除屏幕:

system("clear);

休眠

Window的控制台中休眠,可以调用windows.h中的Sleep(),比如Sleep(1000)函数,这里1000为毫秒数,功能为延时1s后程序向下运行,下面是一个3秒倒计时程序:

for(int i = 3; i >= 1; i--) {
  printf("%d\n", i);
  Sleep(1000);
}

Linux的控制台休眠可以使用stdlib.h中的system()调用Shell命令sleep n, 这里n代表秒数。

Linux下程序需要这样改写:

for(int i = 3; i >= 1; i--) {
  printf("%d\n", i);
  system("sleep 1");
}

windows控制台窗口操作API

Windows.h中定义的用于控制台窗口操作的API函数如下:

  • GetConsoleScreenBufferInfo 获取控制台窗口信息
  • GetConsoleTitle 获取控制台窗口标题
  • ScrollConsoleScreenBuffer 在缓冲区中移动数据块
  • SetConsoleScreenBufferSize 更改指定缓冲区大小
  • SetConsoleTitle 设置控制台窗口标题
  • SetConsoleWindowInfo 设置控制台窗口信息
#include 
SetConsoleTitle("hello world!"); // 设置

由于Linux中往往不安装图形界面的,因此一般也不需要控制控制台窗口,这里就不介绍了。

控制台初始化

#include 
#include 
using namespace std;

int main()
{
    //设置控制台窗口标题
    SetConsoleTitle("hello world!");

    //获取控制台窗口信息;
    //GetConsoleScreenBufferInfo(HANDLE hConsoleOutput, CONSOLE_SCREEN_BUFFER_INFO *bInfo)
    //第一个hConsoleOutput参数(标准控制句柄)通过GetStdHandle()函数返回值获得
    //第二个参数CONSOLE_SCREEN_BUFFER_INFO 保存控制台信息结构体指针
        /*数据成员如下:
        {
            COORD dwSize; // 缓冲区大小
            COORD dwCursorPosition; //当前光标位置
            WORD wAttributes; //字符属性
            SMALL_RECT srWindow; //当前窗口显示的大小和位置
            COORD dwMaximumWindowSize; //最大的窗口缓冲区大小
        }
        */
    HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
    CONSOLE_SCREEN_BUFFER_INFO bInfo;
    GetConsoleScreenBufferInfo(hOutput, &bInfo);
    cout << "窗口缓冲区大小:" << bInfo.dwSize.X << ", " << bInfo.dwSize.Y << endl;
    cout << "窗口坐标位置:" << bInfo.srWindow.Left << ", " << bInfo.srWindow.Top
         << ", "<< bInfo.srWindow.Right << ", " << bInfo.srWindow.Bottom << endl;

    //设置显示区域坐标
    //SetConsoleWindowInfo(HANDLE, BOOL, SMALL_RECT *);
    SMALL_RECT rc = {0,0, 79, 24}; // 坐标位置结构体初始化
    SetConsoleWindowInfo(hOutput,true ,&rc);
    cout << "窗口显示坐标位置:" << bInfo.srWindow.Left << ", " << bInfo.srWindow.Top
         << ", "<< bInfo.srWindow.Right << ", " << bInfo.srWindow.Bottom << endl;

    //更改指定缓冲区大小
    //SetConsoleScreenBufferSize(HANDLE hConsoleOutput, COORD dwSize)
    //COORD为一个数据结构体
    COORD dSiz = {80, 25};
    SetConsoleScreenBufferSize(hOutput, dSiz);
    cout << "改变后大小:" << dSiz.X << ", " << dSiz.Y << endl;

    //获取控制台窗口标题
    //GetConsoleTitle(LPTSTR lpConsoleTitle, DWORD nSize)
    //lpConsoleTitle为指向一个缓冲区指针以接收包含标题的字符串;nSize由lpConsoleTitle指向的缓冲区大小
    char cTitle[255];
    GetConsoleTitleA(cTitle, 255);
    cout << "窗口标题:" << cTitle << endl;

    // 关闭标准输出设备句柄
    CloseHandle(hOut);
    return 0;
}

设置文本颜色和光标移动控制

#include 
#include 
using namespace std;
int main()
{
    /*设置文本属性
    BOOL SetConsoleTextAttribute(HANDLE hConsoleOutput, WORD wAttributes);//句柄, 文本属性*/

    HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE); // 获取标准输出设备句柄
    WORD wr1 = 0xfa;//定义颜色属性;第一位为背景色,第二位为前景色
    SetConsoleTextAttribute(hOut, wr1);
    cout << "hello world!" << endl;

    WORD wr2 = FOREGROUND_RED | FOREGROUND_INTENSITY;//方法二用系统宏定义颜色属性
    SetConsoleTextAttribute(hOut, wr2);
    cout << "hello world!" << endl;

    /*移动文本位置位置
    BOOL ScrollConsoleScreenBuffer(HANDLE hConsoleOutput, CONST SMALL_RECT* lpScrollRectangle, CONST SMALL_RECT* lpClipRectangle,
                                   COORD dwDestinationOrigin,CONST CHAR_INFO* lpFill);
                                  // 句柄// 裁剪区域// 目标区域 // 新的位置// 填充字符*/
    //输出文本
    SetConsoleTextAttribute(hOut, 0x0f);
    cout << "01010101010101010101010101010" << endl;
    cout << "23232323232323232323232323232" << endl;
    cout << "45454545454545454545454545454" << endl;
    cout << "67676767676767676767676767676" << endl;

    SMALL_RECT CutScr = {1, 2, 10, 4}; //裁剪区域与目标区域的集合行成剪切区域
    SMALL_RECT PasScr = {7, 2, 11, 9}; //可以是NULL,即全区域
    COORD pos = {1, 8};     //起点坐标,与裁剪区域长宽构成的区域再与目标区域的集合为粘贴区

    //定义填充字符的各个参数及属性
    SetConsoleTextAttribute(hOut, 0x1);
    CONSOLE_SCREEN_BUFFER_INFO Intsrc;
    GetConsoleScreenBufferInfo(hOut, &Intsrc);
    CHAR_INFO chFill = {'A',  Intsrc.wAttributes}; //定义剪切区域填充字符
    ScrollConsoleScreenBuffer(hOut, &CutScr, &PasScr, pos, &chFill); //移动文本

    CloseHandle(hOut); // 关闭标准输出设备句柄
    return 0;
}

WORD文本属性预定义宏:(可以直接用16进制表示,WORD w = 0xf0;前一位表示背景色,后一位代表前景色)

FOREGROUND_BLUE 蓝色
FOREGROUND_GREEN 绿色
FOREGROUND_RED 红色
FOREGROUND_INTENSITY 加强
BACKGROUND_BLUE 蓝色背景
BACKGROUND_GREEN 绿色背景
BACKGROUND_RED 红色背景
BACKGROUND_INTENSITY 背景色加强
COMMON_LVB_REVERSE_VIDEO 反色

当前文本属性信息可通过调用函数
GetConsoleScreenBufferInfo后,在CONSOLESCREEN BUFFER_INFO结构成员wAttributes中得到。
在指定位置处写属性

BOOL WriteConsoleOutputAttribute(HANDLE hConsoleOutput, CONST WORD *lpAttribute, DWORD nLength, 
                                COORD dwWriteCoord, LPDWORD lpNumberOfAttrsWritten);
                                //句柄, 属性, 个数, 起始位置, 已写个数*/

填充指定数据的字符

BOOL FillConsoleOutputCharacter(HANDLE hConsoleOutput, TCHAR cCharacter,DWORD nLength, 
                               COORD dwWriteCoord, LPDWORD lpNumberOfCharsWritten);
                               // 句柄, 字符, 字符个数, 起始位置, 已写个数*/

在当前光标位置处插入指定数量的字符

BOOL WriteConsole(HANDLE hConsoleOutput, CONST VOID *lpBuffer, DWORD nNumberOfCharsToWrite,
                 LPDWORD lpNumberOfCharsWritten,LPVOID lpReserved);
                 //句柄, 字符串, 字符个数, 已写个数, 保留*/

向指定区域写带属性的字符

BOOL WriteConsoleOutput(HANDLE hConsoleOutput, CONST CHAR_INFO *lpBuffer, COORD dwBufferSize,
                       COORD dwBufferCoord,PSMALL_RECT lpWriteRegion );
                       // 句柄 // 字符数据区// 数据区大小// 起始坐标// 要写的区域*/

在指定位置处插入指定数量的字符

BOOL WriteConsoleOutputCharacter(HANDLE hConsoleOutput, LPCTSTR lpCharacter, DWORD nLength,
                                COORD dwWriteCoord, LPDWORD lpNumberOfCharsWritten);
                                // 句柄// 字符串// 字符个数// 起始位置// 已写个数*/

填充字符属性

BOOL FillConsoleOutputAttribute(HANDLE hConsoleOutput, WORD wAttribute,DWORD nLength,
                               COORD dwWriteCoord, LPDWORD lpNumberOfAttrsWritten);
                               //句柄, 文本属性, 个数, 开始位置, 返回填充的个数*/

设置代码页,代码页是字符集编码的别名,也有人称"内码表"。

SetConsoleOutputCP(437);
//如(简体中文) 设置成936

光标操作控制

#include 
#include 
using namespace std;
int main()
{
    cout << "hello world!" << endl;

    //设置光标位置
    //SetConsoleCursorPosition(HANDLE hConsoleOutput,COORD dwCursorPosition);
    //设置光标信息
    //BOOL SetConsoleCursorInfo(HANDLE hConsoleOutput, PCONST PCONSOLE_CURSOR_INFO lpConsoleCursorInfo);
    //获取光标信息
    //BOOL GetConsoleCursorInfo(HANDLE hConsoleOutput,  PCONSOLE_CURSOR_INFO lpConsoleCursorInfo);
    //参数1:句柄;参数2:CONSOLE_CURSOR_INFO结构体{DWORD dwSize;(光标大小取值1-100)BOOL bVisible;(是否可见)}

    Sleep(2000);//延时函数
    HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE);
    COORD w = {0, 0};
    SetConsoleCursorPosition(hOut, w);
    CONSOLE_CURSOR_INFO cursorInfo = {1, FALSE};
    Sleep(2000);//延时函数
    SetConsoleCursorInfo(hOut, &cursorInfo);
    CloseHandle(hOut); // 关闭标准输出设备句柄
    return 0;
}

键盘操作控制

#include 
#include 
#include 
using namespace std;
HANDLE hOut;
//清除函数
void cle(COORD ClPos)
{
    SetConsoleCursorPosition(hOut, ClPos);
    cout << "            " << endl;
}
//打印函数
void prin(COORD PrPos)
{
    SetConsoleCursorPosition(hOut, PrPos);
    cout << "hello world!" << endl;
}
//移动函数
void Move(COORD *MoPos, int key)
{
    switch(key)
    {
    case 72: MoPos->Y--;break;
    case 75: MoPos->X--;break;
    case 77: MoPos->X++;break;
    case 80: MoPos->Y++;break;
    default: break;
    }
}

int main()
{
    cout << "用方向键移动下行输出内容" << endl;
    hOut = GetStdHandle(STD_OUTPUT_HANDLE);//取句柄
    COORD CrPos = {0, 1};//保存光标信息
    prin(CrPos);//打印
    //等待键按下
    while(1)
    {
        if(kbhit())
        {
            cle(CrPos);//清除原有输出
            Move(&CrPos, getch());
            prin(CrPos);
        }
    }
    return 0;
}

可以用方向键任意移动hello world!

注意区分:

getch()getche()getcher()函数

Linux下控制台操作替代办法

字体颜色

在 Linux 下若想输出 类似与 Windows 下的多颜色字体如何做呢?本文就来介绍实现的方法。
首先,来看下 在Linux 下颜色的表示

注意自定义的配置需在写在\033[和m之间

\033[22;30m - black
\033[22;31m - red
\033[22;32m - green
\033[22;33m - brown
\033[22;34m - blue
\033[22;35m - magenta
\033[22;36m - cyan
\033[22;37m - gray
\033[01;30m - dark gray
\033[01;31m - light red
\033[01;32m - light green
\033[01;33m - yellow
\033[01;34m - light blue
\033[01;35m - light magenta
\033[01;36m - light cyan
\033[01;37m - white

可以看出都是使用的转义字体来实现的。
比如:
Linux 终端输入:echo -e "\033[35;1m Shocking \033[0m"
C代码: printf("\033[34mThis is blue.\033[0m\n");
是不是出错不同的颜色了,记得最后要 "\033[0m" 关闭所有属性,这样又回到了系统默认的颜色了。

一个定义宏的办法如下:([0;是用来清除之前或之后的设置)

#define COLOR(msg, code) "\033[0;1;" #code "m" msg "\033[0m"
#define RED(msg)    COLOR(msg, 31)
#define GREEN(msg)  COLOR(msg, 32)
#define YELLOW(msg) COLOR(msg, 33)
#define BLUE(msg)   COLOR(msg, 34)

光标控制

Linux 下终端 C 语言控制光标的技巧

// 清除屏幕
#define CLEAR() printf("\033[2J")

// 上移光标
#define MOVEUP(x) printf("\033[%dA", (x))

// 下移光标
#define MOVEDOWN(x) printf("\033[%dB", (x))

// 左移光标
#define MOVELEFT(y) printf("\033[%dD", (y))

// 右移光标
#define MOVERIGHT(y) printf("\033[%dC",(y))

// 定位光标

#define MOVETO(x,y) printf("\033[%d;%dH", (x), (y))

// 光标复位

#define RESET_CURSOR() printf("\033[H")

// 隐藏光标

#define HIDE_CURSOR() printf("\033[?25l")

// 显示光标

#define SHOW_CURSOR() printf("\033[?25h")

//反显

#define HIGHT_LIGHT() printf("\033[7m")

#define UN_HIGHT_LIGHT() printf("\033[27m")

模拟按键监听

windows平台接受字符并不回显可以调用<conio.h>库的getch函数, 但是这是依赖于windows的BIOS。
linux下可以使用命令stty -echo来关闭回显,当接受字符后,使用stty echo命令恢复设置即可.

linux下如何向windows的getch或者getche一样,不需要回车就可以接受字符?
这里可以使用命令raw开启一次接受一个字符的模式,接受字符后,再次使用cooked(回车之后一锅端模式)模式即可

#include 
#include 
#define ENTER_KEY 13
#define ESCAPE_KEY 27

char getch()
{
    char ch;
    system("stty -echo raw");
    ch = getchar();
    system("stty echo cooked");
    return ch;
}

int main(int argc, char *argv[])
{
    char ch;

    system("clear\n");
    while (1)
    {
        printf("Press Enter to continue, ESC to exit\n");
        ch = getch();
        if (ch == ESCAPE_KEY)
            break;
        if (ch == ENTER_KEY)
        {
            printf("Input a character\n");
            ch = getch();
            printf("=%c\n", ch);
        }
    }

    return 0;
}

Hangman 猜词游戏

我们将从构建经典的猜词游戏,需要有两个玩家,一个玩家负责出题,另一个玩家负责猜。如果你从来没有听过这个游戏,

规则

  1. 在给定的选词范围内,比如"动物"或""国家",玩家一选择一个秘密单词并根据字母数画相应数量的短横,即“”,短横用来表示单词中的每个字母。比如秘密单词是"cat"则可以用" "表示。

  2. 游戏开始后,玩家二猜一个字母,玩家一判断这个字符是否在秘密单词中出现,如果出现则将对应位置的短横替换成正确的字符。比如猜的是a,则表示成_ _ a

  3. 如果玩家二猜错了字母,玩家一会逐步从上到下的完成一副小人上吊的图像,如果玩家2在完整的图像被画出来之前猜出来秘密单词的所有字母他就赢了,否则当完整的图像画出来之后,小人的脚离开地面就预示着玩家二输了。

    第1次猜错

    ________   

    第2次猜错

    ________   
    |      |   

    第3次猜错

    ________   
    |      |   
    |      0   

    第4次猜错

    ________   
    |      |   
    |      0   
    |     /|\  

    第5次猜错

    ________   
    |      |   
    |      0   
    |     /|\  
    |     / \  

    第6次猜错,小人脚离地,玩家二输了,游戏结束

    ________   
    |      |   
    |      0   
    |     /|\  
    |     / \  
    |          
  4. 如果玩家在小人脚离地之前猜出了所有字母,例如 c a t.则玩家二获胜。

注意:以下代码是在linux环境中运行:

游戏的初始化
// 初始化词库
char words[5][100] = {"china", "japan", "india", "korea", "syria"};
// 初始化词库的数量
int words_num = sizeof(words) / sizeof(words[0]);
// 初始化要猜的词
char secret_word[100] = "";
// 初始化要猜的词的字母数
int secret_word_len = 0;
// 显示的题板, 未猜出的字母用_代替
// 打印的上吊小人的画像
char stages[][15] = {"  ________    ",   // stage 1
                     "  |      |    ",   // stage 2
                     "  |      0    ",   // stage 3
                     "  |     /|\\  ",   // stage 4
                     "  |     / \\  ",   // stage 5
                     "  |________   "};  // stage 6
int stage_num     = sizeof(stages) / sizeof(stages[0]);
// 初始化猜错的次数
int wrong_guess = 0;
// 初始化是否赢得游戏
bool win = false;

// FUNCTION PROTOTYPES
void PrintBoard(int stage);
void StartGame();
char getch();

int main(int argc, char *argv[]) {

    // init
    srand((unsigned int)time(NULL));
    int random_index = rand() % words_num;
    strcpy(secret_word, words[random_index]);
    secret_word_len = strlen(secret_word);

    StartGame();
    return 0;
}
玩法部分

游戏的玩法部分封装到了StartGame函数中,玩法部分通常是个循环


// GAME LOOP
while (wrong_guess < stage_num) {
...
}

判断是否猜中

char *find_pos = strstr(remaining_letters, guess);
if (find_pos != NULL) {
    int pos                = find_pos - remaining_letters;
    remaining_letters[pos] = '$';
    letter_board[pos]      = guess[0];
}
else {
    wrong_guess++;
    PrintBoard(wrong_guess);
    system("sleep 1");
}

如果输了就打印上吊图像


void PrintBoard(int stage) {
    for (int i = 0; i < stage_num; i++) {
        if (i < stage)
            printf("%s\n", stages[i]);
        else
            puts("");
    }
}

练习

你也可以试着完成一个属于你自己的控制台小游戏, 多花些时间做些类似的练习可以帮助你提高编程能力,同时也能让你对编程保持兴趣。

我还记得在DOS时期(95年左右)我第一个使用QuickBasic语言制作的猜飞机头的游戏,虽然是简单的游戏但是最终被我制作的越来越复杂,比如增加关卡,添加声效,游戏记录的保存等,可惜是存储在软盘中现在已经遗失了。

  • 三子棋或者五子棋游戏

  • 根据心情选歌

  • 根据品牌打印广告语。

寻求帮助

如果你写代码时卡壳,大多数的问题通过百度都可以解决,不行的话就CSDN或者知乎。

如果英文水平不赖就科学上网去谷歌搜索,这里要强调的是浏览Stack Exchange这个神奇的网站会很有帮助。有两个版块非常有用。

一个是Stack Overflow,程序员应该没有不知道它的,上面几乎涵盖所有编程可能遇到的问题。即便你的问题是独一无二其他人都没有经历过的,这是一个QA类型的网站,你可以在上面发布问题,会有大佬帮你解答的。

https://www.codementor.io/ama/0926528143/stackoverflow-python-moderator-martijn-pieters-zopatista

http://programmers.stackexchange.com/questions/44177/what-is-the-single-most-effective-thing-you-did-to-improve-your-programming-skil

另外一个有用的板块叫Code Review, 你只要发布你的代码就会有人给你"指手画脚", 告诉你哪些地方做的好,哪些地方做的不好,如何改进之类的建议。

Views: 18