1

我正在为我的第一年作业制作一个简单的糖果迷恋游戏。

board[5][5]我正处于这个阶段,一旦程序执行,我需要在板的中心()显示我自制的简单标记(*box made of '|' and '_'*) 。

这是当前代码:

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

//FUNCTION: Draw the Board
int drawBoard()
{
    //Declare array size
    int board[9][9];

    //initialize variables
    int rows, columns, randomNumber, flag;

    //random number seed generator
    srand(time(NULL));

        for ( rows = 0 ; rows < 9 ; rows++ )
        {

            for ( columns = 0 ; columns < 9 ; columns++ )
            {
                flag = 0;

                do
               {
                    //generate random numbers from 2 - 8
                randomNumber = rand() %7 + 2;

                board[rows][columns] = randomNumber;

                //Checks for 2 adjacent numbers.
                if  ( board[rows][columns] == board[rows - 1][columns] || board[rows][columns] == board[rows][columns - 1] )
                    {
                        flag = 0;
                        continue;
                    }

                else
                     {
                        flag = 1;
                        printf( "  %d  ", board[rows][columns] );
                     }

                } while ( flag == 0 );

            }//end inner for-loop

            printf("\n\n");

        }//end outer for-loop

//call FUNCTION marker() to display marker around board[5][5]
marker( board[5][5] );

}//end FUNCTION drawBoard

//FUNCTION: Mark the surrounding of the number with "|" and "_" at board[5][5]
void marker( int a )
{
    printf( " _ \n" );
    printf( "|%c|\n", a );
    printf( " _ \n" );
}

int main()
{
    drawBoard();
}

在函数结束时drawBoard(),我放置了代码marker( board[5][5] )

这应该已经在坐标处打印的数字周围打印了标记board[5][5]..但由于某种原因,它会在电路板打印后立即显示。

那么为什么它不在那个坐标上打印,虽然我指定了它board[5][5]

这里可能是什么问题?

4

2 回答 2

0

所以在您的标记功能中,您需要传递板和要打印的坐标

void marker( int x, int y, int** board )
{
    board[x][y-1]="_";
    board[x-1][y]="|";
    board[x+1][y]="|";
    board[x][y+1]="_";
}

然后在调用marker(5,5,board)之后,再次调用drawboard

我的代码有点不对劲,但这就是逻辑,除非您需要检查标记是否位于电路板边缘

换句话说,您需要保留电路板,并且每当您对其进行更改时,请清除屏幕并再次打印整个电路板。

于 2013-10-23T20:34:28.120 回答
0

你这样做的方式没有持久的绘图。您只是直接打印到 shell/命令提示符。你试图做事的方式是行不通的。绘制后无法编辑根据提示绘制的内容,您需要基本上清除屏幕然后再次绘制,但使用指定的制造商。

我不知道你是否能够在你的作业中使用库,但是一个非常好的库可以让你这样做是ncurses

编辑 完全重写答案


在 CMD 中将事物相互叠加

好吧,我在工作中有一些停机时间,所以我编写了一个项目来做你需要的事情,我将发布代码并解释它的作用以及你为什么需要它。

首先,您将需要一个基本的渲染缓冲区或渲染上下文。每当您在诸如 OpenGL 之类的图形 API 中进行编程时,您不只是直接渲染到屏幕上,而是将您拥有的每个对象渲染到一个缓冲区,该缓冲区将您的内容光栅化并将其转换为像素。一旦采用这种形式,API 就会将渲染的图片推送到屏幕上。我们将采用类似的方法,而不是绘制到 GPU 上的像素缓冲区,而是绘制到字符缓冲区。将每个字符视为屏幕上的一个像素。

这是完整源代码的 pastebin: 项目的完整源代码


RenderContext

我们班会做这件事的RenderContext班。它具有保存宽度和高度的字段以及一个数组chars和一个特殊的字符,我们在清除缓冲区时填充缓冲区。

这个类只包含一个数组和函数,让我们渲染它。它确保当我们吸引它时,我们在界限之内。对象可能会尝试在剪切空间之外(屏幕外)进行绘制。但是,在那里绘制的任何内容都将被丢弃。

class RenderContext {
private:
   int m_width, m_height; // Width and Height of this canvas
   char* m_renderBuffer; // Array to hold "pixels" of canvas
   char m_clearChar; // What to clear the array to

public:
   RenderContext() : m_width(50), m_height(20), m_clearChar(' ') {
      m_renderBuffer = new char[m_width * m_height];
   }
   RenderContext(int width, int height) : m_width(width), m_height(height), m_clearChar(' ') {
      m_renderBuffer = new char[m_width * m_height];
   }
   ~RenderContext();
   char getContentAt(int x, int y);
   void setContentAt(int x, int y, char val);
   void setClearChar(char clearChar);
   void render();
   void clear();
};

这个类的两个最重要的功能是setContentAtrender

setContentAt是一个对象调用来填充“像素”值。为了使它更灵活一点,我们的类使用指向字符数组的指针,而不是直接数组(甚至是二维数组)。这让我们可以在运行时设置画布的大小。因此,我们访问此数组的元素,用x + (y * m_width)它替换二维取消引用,例如arr[i][j]

// Fill a specific "pixel" on the canvas
void RenderContext::setContentAt(int x, int y, char val) {
   if (((0 <= x) && (x < m_width)) && ((0 <= y) && (y < m_height))) {
      m_renderBuffer[(x + (y * m_width))] = val;
   }
}

render是真正吸引提示的原因。它所做的只是遍历缓冲区中的所有“像素”并将它们放在屏幕上,然后移动到下一行。

// Paint the canvas to the shell
void RenderContext::render() {
   int row, column;
   for (row = 0; row < m_height; row++) {
      for (column = 0; column < m_width; column++) {
         printf("%c", getContentAt(column, row));
      }
      printf("\n");
   }
}

I_Drawable

我们的下一个类是Interface允许我们与可以绘制到我们的 RenderContext 的对象进行合同。它是纯虚拟的,因为我们不想实际实例化它,我们只想从中派生。它的唯一功能是draw接受 RenderContext。派生类使用此调用来接收 RenderContext,然后使用 RenderContext 的 setContentAt 将“像素”放入缓冲区。

class I_Drawable {
public:
   virtual void draw(RenderContext&) = 0;
};

GameBoard

第一个实现 I_Drawable 从而能够渲染到我们的 RenderContext 的类是 GameBoard 类。这是大部分逻辑的用武之地。它具有宽度、高度字段和一个整数数组,该数组保存板上元素的值。它还有另外两个用于间距的字段。因为当您使用代码绘制电路板时,每个元素之间都有空格。我们不需要将它合并到板子的底层结构中,我们只需要在绘制时使用它们。

class GameBoard : public I_Drawable {
private:
   int m_width, m_height; // Width and height of the board
   int m_verticalSpacing, m_horizontalSpacing; // Spaces between each element on the board
   Marker m_marker; // The cursor that will draw on this board
   int* m_board; // Array of elements on this board

   void setAtPos(int x, int y, int val);
   void generateBoard();

public:
   GameBoard() : m_width(10), m_height(10), m_verticalSpacing(5), m_horizontalSpacing(3), m_marker(Marker()) {
      m_board = new int[m_width * m_height];
      generateBoard();
   }
   GameBoard(int width, int height) : m_width(width), m_height(height), m_verticalSpacing(5), m_horizontalSpacing(3), m_marker(Marker()) {
      m_board = new int[m_width * m_height];
      generateBoard();
   }
   ~GameBoard();
   int getAtPos(int x, int y);
   void draw(RenderContext& renderTarget);
   void handleInput(MoveDirection moveDirection);
   int getWidth();
   int getHeight();
};

它的关键函数是generateBoard,handleInput和派生的虚函数draw。但是,请注意,它在其构造函数中创建了一个新的 int 数组并将其提供给它的指针。然后它的析构函数会在板子消失时自动删除分配的内存。

generateBoard是我们用来实际创建板并用数字填充的。它将遍历板上的每个位置。每次,它都会直接查看左侧和上方的元素并存储它们。然后它将生成一个随机数,直到它生成的数字与存储的任何一个元素都不匹配,然后它将该数字存储在数组中。我重写了这个以摆脱标志的使用。这个函数在类的构造过程中被调用。

// Actually create the board
void GameBoard::generateBoard() {
   int row, column, randomNumber, valToLeft, valToTop;

   // Iterate over all rows and columns
   for (row = 0; row < m_height; row++) {
      for (column = 0; column < m_width; column++) {
         // Get the previous elements
         valToLeft = getAtPos(column - 1, row);
         valToTop = getAtPos(column, row - 1);

         // Generate random numbers until we have one 
         // that is not the same as an adjacent element
         do {
            randomNumber = (2 + (rand() % 7));
         } while ((valToLeft == randomNumber) || (valToTop == randomNumber));
         setAtPos(column, row, randomNumber);
      }
   }
}

handleInput是处理在板上移动光标的方法。它基本上是一个免费赠品,是您在将光标移到板上之后的下一步。我需要一种方法来测试绘图。它接受一个枚举,我们打开它以知道将光标移动到下一个位置。如果您可能希望在到达边缘时让光标环绕在棋盘上,您会想在此处执行此操作。

void GameBoard::handleInput(MoveDirection moveDirection) {
   switch (moveDirection) {
      case MD_UP:
         if (m_marker.getYPos() > 0)
            m_marker.setYPos(m_marker.getYPos() - 1);
         break;
      case MD_DOWN:
         if (m_marker.getYPos() < m_height - 1)
            m_marker.setYPos(m_marker.getYPos() + 1);
         break;
      case MD_LEFT:
         if (m_marker.getXPos() > 0)
            m_marker.setXPos(m_marker.getXPos() - 1);
         break;
      case MD_RIGHT:
         if (m_marker.getXPos() < m_width - 1)
            m_marker.setXPos(m_marker.getXPos() + 1);
         break;
   }
}

draw非常重要,因为它是将数字输入 RenderContext 的原因。总而言之,它遍历板上的每个元素,并在画布上的正确位置绘制,将元素放置在正确的“像素”下。这是我们合并间距的地方。另外,请注意我们在此函数中渲染光标。

这是一个选择问题,但是您可以在 GameBoard 类之外存储一个标记并在主循环中自己渲染它(这将是一个不错的选择,因为它放松了 GameBoard 类和 Marker 类之间的耦合。但是,由于它们相当耦合的,我选择让 GameBoard 渲染它。如果我们使用场景图,就像我们可能会使用更复杂的场景/游戏一样,Marker 可能是 GameBoard 的子节点,所以它类似于这个实现,但通过不在 GameBoard 类中存储显式标记而更加通用。

// Function to draw to the canvas
void GameBoard::draw(RenderContext& renderTarget) {
   int row, column;
   char buffer[8];

   // Iterate over every element
   for (row = 0; row < m_height; row++) {
      for (column = 0; column < m_width; column++) {

         // Convert the integer to a char
         sprintf(buffer, "%d", getAtPos(column, row));

         // Set the canvas "pixel" to the char at the
         // desired position including the padding
         renderTarget.setContentAt(
                 ((column * m_verticalSpacing) + 1),
                 ((row * m_horizontalSpacing) + 1),
                 buffer[0]);
      }
   }

   // Draw the marker
   m_marker.draw(renderTarget);
}

Marker

说到Marker班级,现在让我们来看看。Marker 类实际上与 GameBoard 类非常相似。但是,它缺少很多 GameBoard 所具有的逻辑,因为它不需要担心板上的一堆元素。重要的是绘图功能。

class Marker : public I_Drawable {
private:
   int m_xPos, m_yPos; // Position of cursor
public:
   Marker() : m_xPos(0), m_yPos(0) {
   }
   Marker(int xPos, int yPos) : m_xPos(xPos), m_yPos(yPos) {
   }
   void draw(RenderContext& renderTarget);
   int getXPos();
   int getYPos();
   void setXPos(int xPos);
   void setYPos(int yPos);
};

draw只需将四个符号放在 RenderContext 上以勾勒板上选定的元素。请注意,Marker 对 GameBoard 类一无所知。它没有引用它,它不知道它有多大,或者它包含什么元素。不过你应该注意到,我变得懒惰并且没有取出硬编码的偏移量,这种偏移量取决于 GameBoard 的填充。您应该为此实施更好的解决方案,因为如果您更改 GameBoard 类中的填充,您的光标将关闭。

除此之外,每当绘制符号时,它们都会覆盖 ContextBuffer 中的任何内容。这很重要,因为您问题的重点是如何在 GameBoard 上绘制光标。这也说明了绘制顺序的重要性。假设每当我们绘制 GameBoard 时,我们在每个元素之间绘制了一个“=”。如果我们先绘制光标再绘制棋盘,GameBoard 会在光标上方绘制,使其不可见。

如果这是一个更复杂的场景,我们可能需要做一些花哨的事情,比如使用深度缓冲区来记录z-index元素的值。然后,每当我们绘制时,我们都会检查新元素的 z-index 是否比 RenderContext 缓冲区中已有的内容更近或更远。根据这一点,我们可能会完全跳过绘制“像素”。

但是我们没有,所以请注意订购您的抽奖电话!

// Draw the cursor to the canvas
void Marker::draw(RenderContext& renderTarget) {

   // Adjust marker by board spacing
   // (This is kind of a hack and should be changed)
   int tmpX, tmpY;
   tmpX = ((m_xPos * 5) + 1);
   tmpY = ((m_yPos * 3) + 1);

   // Set surrounding elements
   renderTarget.setContentAt(tmpX - 0, tmpY - 1, '-');
   renderTarget.setContentAt(tmpX - 1, tmpY - 0, '|');
   renderTarget.setContentAt(tmpX - 0, tmpY + 1, '-');
   renderTarget.setContentAt(tmpX + 1, tmpY - 0, '|');
}

CmdPromptHelper

我要讨论的最后一个类是 CmdPromptHelper。你原来的问题没有这样的东西。但是,您很快就会担心它。这个类也只在 Windows 上有用,所以如果你在 linux/unix 上,你需要担心自己处理绘制到 shell 的问题。

class CmdPromptHelper {
private:
   DWORD inMode; // Attributes of std::in before we change them
   DWORD outMode; // Attributes of std::out before we change them
   HANDLE hstdin; // Handle to std::in
   HANDLE hstdout; // Handle to std::out
public:
   CmdPromptHelper();
   void reset();
   WORD getKeyPress();
   void clearScreen();
};

每一项功能都很重要。构造函数获取当前命令提示符的std::in和句柄。std::outgetKeyPress函数返回用户按下的(键向上事件被忽略)。并且该clearScreen函数清除提示(实际上,它实际上将提示中已经存在的任何内容向上移动)。

getKeyPress只需确保您有一个句柄,然后读取已输入控制台的内容。它确保无论它是什么,它都是一个键并且它被按下。然后它将密钥代码作为 Windows 特定的枚举返回,通常以VK_.

// See what key is pressed by the user and return it
WORD CmdPromptHelper::getKeyPress() {
   if (hstdin != INVALID_HANDLE_VALUE) {
      DWORD count;
      INPUT_RECORD inrec;

      // Get Key Press
      ReadConsoleInput(hstdin, &inrec, 1, &count);

      // Return key only if it is key down
      if (inrec.Event.KeyEvent.bKeyDown) {
         return inrec.Event.KeyEvent.wVirtualKeyCode;
      } else {
         return 0;
      }

      // Flush input
      FlushConsoleInputBuffer(hstdin);
   } else {
      return 0;
   }
}

clearScreen有点骗人。您会认为它会清除提示中的文本。据我所知,它没有。我很确定它实际上将所有内容向上移动,然后在提示符中写入大量字符,以使其看起来像屏幕已被清除。

这个函数提出的一个重要概念是缓冲渲染的概念。同样,如果这是一个更健壮的系统,我们将希望实现双缓冲的概念,这意味着渲染到一个不可见的缓冲区并等待直到所有绘制完成,然后将不可见的缓冲区与可见的缓冲区交换。这使得渲染视图更加清晰,因为我们在它们仍在绘制时看不到它们。我们在这里做事的方式,我们看到渲染过程发生在我们面前。这不是一个主要问题,它有时看起来很丑。

// Flood the console with empty space so that we can
// simulate single buffering (I have no idea how to double buffer this)
void CmdPromptHelper::clearScreen() {
   if (hstdout != INVALID_HANDLE_VALUE) {
      CONSOLE_SCREEN_BUFFER_INFO csbi;
      DWORD cellCount; // How many cells to paint
      DWORD count; // How many we painted
      COORD homeCoord = {0, 0}; // Where to put the cursor to clear

      // Get console info
      if (!GetConsoleScreenBufferInfo(hstdout, &csbi)) {
         return;
      }

      // Get cell count
      cellCount = csbi.dwSize.X * csbi.dwSize.Y;

      // Fill the screen with spaces
      FillConsoleOutputCharacter(
              hstdout,
              (TCHAR) ' ',
              cellCount,
              homeCoord,
              &count
              );

      // Set cursor position
      SetConsoleCursorPosition(hstdout, homeCoord);
   }
}

main

您需要担心的最后一件事是如何使用所有这些东西。这就是 main 的用武之地。你需要一个游戏循环。游戏循环可能是任何游戏中最重要的事情。你看到的任何游戏都会有一个游戏循环。

这个想法是:

  1. 在屏幕上显示一些东西
  2. 读取输入
  3. 处理输入
  4. 转到 1

这个程序也不例外。它做的第一件事是创建一个 GameBoard 和一个 RenderContext。它还制作了一个 CmdPromptHelper,它可以与命令提示符交互。之后,它开始循环并让循环继续,直到我们达到退出条件(对我们来说,这是按退出)。我们可以有一个单独的类或函数来分派输入,但是由于我们只是将输入分派给另一个输入处理程序,所以我将它保留在主循环中。获得输入后,将 if 发送到 GameBoard,GameBoard 会相应地改变自身。下一步是清除 RenderContext 和屏幕/提示。如果没有按下转义,则重新运行循环。

int main() {
   WORD key;
   GameBoard gb(5, 5);
   RenderContext rc(25, 15);
   CmdPromptHelper cph;
   do {
      gb.draw(rc);
      rc.render();

      key = cph.getKeyPress();
      switch (key) {
         case VK_UP:
            gb.handleInput(MD_UP);
            break;
         case VK_DOWN:
            gb.handleInput(MD_DOWN);
            break;
         case VK_LEFT:
            gb.handleInput(MD_LEFT);
            break;
         case VK_RIGHT:
            gb.handleInput(MD_RIGHT);
            break;
      }
      rc.clear();
      cph.clearScreen();
   } while (key != VK_ESCAPE);
}

在你考虑了所有这些事情之后,你就会明白为什么以及在哪里需要绘制你的光标。这不是一个接一个地调用函数的问题,您需要合成您的绘图。你不能只画游戏板然后画标记。至少不是在命令提示符下。我希望这有帮助。它确实减轻了工作中的停机时间。

于 2013-10-23T20:35:36.247 回答