【问题标题】:How can I refresh the console in the most efficient way? (snake game c++)如何以最有效的方式刷新控制台? (蛇游戏c++)
【发布时间】:2018-11-16 21:38:39
【问题描述】:

这是我的蛇代码。 system("cls") 一点效率都没有,控制台闪烁……

#include <iostream>
#include <string>
#include <windows.h>
#include <cstdlib>
#include <ctime>
#include <conio.h>
using namespace std;

bool status = false, win = false;

struct Snake {
    int index_i;
    int index_j;
};

class Game {
private:
    enum eDir { UP, RIGHT, DOWN, LEFT };
    eDir direction;
    const int height = 25, width = 50, max_size = (height - 2)*(width - 2);
    int snake_size = 1, food_x, food_y, snake_x, snake_y, score, speed;
    char snake = '@', food = '*', frame = '#';
    Snake *snake_body = new Snake[max_size];
public:
    Game() {
        snake_x = height / 2;
        snake_y = width / 2;
        snake_body[0].index_i = snake_x;
        snake_body[0].index_j = snake_y;
        PutFood();
    }
    ~Game() {
        delete[] snake_body;
    }
    void DrawTable() {
        system("cls");
        for (int i = 0; i < height; i++) {
            for (int j = 0; j < width; j++) {
                if (!i || i == height - 1 || !j || j == width - 1) {
                    cout << frame;
                }
                else if (i == food_x && j == food_y) {
                    cout << food;
                }
                else if (Check(i, j)) {
                    cout << snake;
                }
                else {
                    cout << " ";
                }
            }
            cout << endl;
        }
        cout << "Your current score is: " << score;
    }
    void Control() {
        if (_kbhit()) {
            switch (_getch()) {
            case 'w':
                direction = UP;
                break;
            case 'a':
                direction = LEFT;
                break;
            case 's':
                direction = DOWN;
                break;
            case 'd':
                direction = RIGHT;
                break;
            }
        }
    }
    void Process() {
        switch (direction) {
        case UP:
            snake_x--;
            Move();
            break;
        case LEFT:
            snake_y--;
            Move();
            break;
        case DOWN:
            snake_x++;
            Move();
            break;
        case RIGHT:
            snake_y++;
            Move();
            break;
        }
    }
    void Move() {
        /*for (int i = 0; i < snake_size; i++) {   tail collision logic (if you try to reverse your move, you die). Optional.
            if (snake_body[i].index_i == snake_x && snake_body[i].index_j == snake_y) {
                status = true;
                return;
            }
        }*/
        snake_body[snake_size].index_i = snake_x;
        snake_body[snake_size].index_j = snake_y;
        if (!snake_x || snake_x == height - 1 || !snake_y || snake_y == width - 1) { // collision logic
            status = true;
        }
        else if (snake_x == food_x && snake_y == food_y) {
            snake_size++;
            score++;
            if (snake_size == max_size) {
                win = true;
                return;
            }
            PutFood();
        }
        else {
            for (int index = 0; index < snake_size; index++) {
                snake_body[index].index_i = snake_body[index + 1].index_i;
                snake_body[index].index_j = snake_body[index + 1].index_j;
            }
            snake_body[snake_size].index_i = 0;
            snake_body[snake_size].index_j = 0;
        }
        Sleep(speed);
    }
    void PutFood() {
        srand(time(NULL));
        food_x = rand() % (height - 2) + 2;
        food_y = rand() % (width - 2) + 2;
    }
    bool Check(int i, int j) {
        for (int k = 0; k < snake_size; k++) {
            if (i == snake_body[k].index_i && j == snake_body[k].index_j) {
                return true;
            }
        }
        return false;
    }
    int getScore() {
        return score;
    }
    void setSpeed(int s) {
        speed = s;
    }
};

int main() {
    Game snake_game;
    char exit;
    string error = "Invalid choice, please choose 1-3";
    int speed, choice;
    cout << "Contol: WASD" << endl << "Set the difficulty level: " << endl << "1. Easy" << endl << "2. Normal" << endl << "3. Hard" << endl;
label:
    cin >> choice;
    try {
        if (choice < 1 || choice > 3) throw error;
    }
    catch (char *error) {
        cout << error << endl;
        goto label;
    }
    switch (choice) {
    case 1:
        speed = 250;
        break;
    case 2:
        speed = 75;
        break;
    case 3:
        speed = 0;
        break;
    }
    snake_game.setSpeed(speed);
    while (!status && !win) {
        snake_game.DrawTable();
        snake_game.Control();
        snake_game.Process();
    }
    if (status && !win) {
        system("cls");
        cout << "YOU LOST! Your score is: " << snake_game.getScore() << endl;
    }
    if (win) {
        system("cls");
        cout << "Congratulations! You won the game!" << endl << "Your score is: " << snake_game.getScore() << endl;
    }
    cin >> exit;
    return 0;
}

【问题讨论】:

  • 在标准 C++ 中,您的选择非常有限,因为该语言中没有“控制台”的概念。 system("cls"); 实际上是运行整个 Windows 程序来清除控制台。您可以考虑使用单独的图形库进行渲染,但如果您开始使用 C++,这可能会非常令人困惑。
  • Windows提供多种控制台处理功能-docs.microsoft.com/en-us/windows/console/console-functions
  • 而要重绘屏幕,您只需要擦除尾部并添加新头部即可。你不需要重新绘制整个东西。

标签: c++ winapi console


【解决方案1】:

system("cls") 很慢。此外,您不需要刷新整个屏幕,因为其中大部分不会改变每一帧。我看到你已经包含 windows.h 所以我猜你只需要它在 Windows 上工作。因此,我建议使用 Windows API 中的函数SetConsoleCursorPosition

这是一个例子

   SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), {10, 10});
   std::cout << ' ';

此代码会将光标位置更改为坐标 (10, 10) 并输出一个空格。 你可以对你想改变的每个“像素”,每一帧都这样做。

【讨论】:

  • +1。我将忽略可移植性问题,因为这个答案使用了系统特定的功能(有充分的理由),但std::cout &lt;&lt; ' '; 不需要解释 32 的含义。 In general avoid Magic Numbers.
  • 这对于每个地方都执行std::cout &lt;&lt; (char)32; 效率非常低
  • 我已编辑我的答案以删除幻数和不必要的演员表。
  • 这没有任何改变——每个std::cout &lt;&lt; ' '——都是对远程进程的单独调用——这很重。执行数百或数千次调用(针对每个光标位置)+ 移动光标位置 - 远程调用和调用 - 效率非常低。这根本不是解决方案
  • 混合调用 Windows 的控制台 API 和 C++ 的流实现是脆弱的。更重要的是,std::cout 通常是缓冲的,因此在刷新缓冲区之前您不会看到屏幕的任何更新。这可能不是您在互动游戏中想要的。
【解决方案2】:

在 Unix 系统上,curses 是实现像您这样的基于文本的程序的经典方法:

使用 curses,程序员可以编写基于文本的应用程序 无需直接为任何特定的终端类型编写。诅咒 执行系统上的库发送正确的控制字符 根据终端类型。它提供了一个或多个的抽象 映射到终端屏幕的窗口。每个窗口都表示 通过字符矩阵。程序员设置所需的外观 每个窗口,然后告诉 curses 包更新屏幕。 库确定了需要的最小更改集 更新显示,然后使用终端的 特定的能力和控制序列。 [维基百科]

显然正在开发一个名为PDCurses 的Windows 端口;你可以看看它是否满足你的需求。

【讨论】:

    【解决方案3】:

    system("cls")实际上是运行整个Windows程序(cmd.exe)来清除控制台。这当然效率很低。相反,我们只需要简单地做同样的事情,就像 cls 命令在 cmd.exe 中所做的那样。对于清除屏幕,我们可以使用ScrollConsoleScreenBuffer - 将控制台屏幕缓冲区的内容替换为空格

    BOOL cls()
    {
        HANDLE hConsoleOutput = GetStdHandle(STD_OUTPUT_HANDLE);
        CONSOLE_SCREEN_BUFFER_INFO csbi;
        if (GetConsoleScreenBufferInfo(hConsoleOutput, &csbi))
        {
            CHAR_INFO fi = { ' ', csbi.wAttributes };
            csbi.srWindow.Left = 0;
            csbi.srWindow.Top = 0;
            csbi.srWindow.Right = csbi.dwSize.X - 1;
            csbi.srWindow.Bottom = csbi.dwSize.Y - 1;
            return ScrollConsoleScreenBufferW(hConsoleOutput, &csbi.srWindow, 0, csbi.dwSize, &fi);
        }
        return FALSE;
    }
    

    【讨论】:

      【解决方案4】:

      system("cls")的效率太低了。以下是清洁屏幕的类似方法:

          //First get the console handle and its info.
          HANDLE hConsoleOut = GetStdHandle(STD_OUTPUT_HANDLE);
          GetConsoleScreenBufferInfo(hConsoleOut, &csbiInfo);
      
          //Fill with ' ' in the whole console(number = X*Y).
          FillConsoleOutputCharacter(hConsoleOut, ' ', csbiInfo.dwSize.X * csbiInfo.dwSize.Y, home, &dummy);
          csbiInfo.dwCursorPosition.X = 0;
          csbiInfo.dwCursorPosition.Y = 0;
      
          //Set the Cursor Position to the Beginning.
          SetConsoleCursorPosition(hConsoleOut, csbiInfo.dwCursorPosition);
      

      【讨论】:

        【解决方案5】:

        当您使用 conio.h 时,您可以使用 gotoxy(x, y) 转到要删除的坐标,然后执行带有空格的 printf(" ")

        【讨论】:

        • gotoxy 来自 Borland 编译器,希望没人再使用它。有一种方法可以在 here 提到的 Windows 上模拟此功能。
        • @alterigel:是的,它来自 Borland。但是,显然它是可用的,它是一种选择。我看不出它不应该用于学生项目的原因......
        • 在使用系统调用和针对现代计算机系统(如 curses)的控制台库失败后,我认为这是最后的手段。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2022-12-31
        • 2012-07-13
        • 1970-01-01
        • 2018-07-02
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多