我正在为我的第一年工作做一个简单的糖果粉碎游戏。
我正处于这个阶段,我需要在程序开始时在棋盘中心(board[5][5]
)上显示我自制的简单标记(*由'|'和'_'*组成的框)执行。
以下是当前代码:
#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]
指定了它?
这可能是什么问题?
答案 0 :(得分: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
我的代码有点偏,但这是逻辑,除了你需要检查标记位于电路板边缘的情况
换句话说,你需要保持一块板,并且每当你改变它时,清除屏幕并再次打印整个板。
答案 1 :(得分:0)
您执行此操作的方式没有持久性绘图。您只是直接打印到shell /命令提示符。你尝试做事的方式是行不通的。在绘制完成后,您无法编辑绘制到提示的内容,您需要基本清除屏幕,然后再次使用指定的制作者进行绘制。
我不知道你是否能够在作业中使用图书馆,但是一个非常好的图书馆可以让你做ncurses
编辑 完全重写答案
好吧,我在工作中遇到了一些停机时间,所以我写了一个项目来做你需要的事情,我将发布代码并解释它的作用以及为什么你需要它。
首先,你需要一个基本上是渲染缓冲区或渲染上下文。无论何时使用OpenGL等图形API进行编程,都不能直接渲染到屏幕上,而是将每个对象渲染到一个缓冲区中,该缓冲区会对内容进行光栅化并将其转换为像素。一旦它以该形式出现,API就会将渲染的图片推到屏幕上。我们将采用类似的方法,而不是绘制到GPU上的像素缓冲区,我们将绘制到字符缓冲区。将每个角色视为屏幕上的一个像素。
以下是完整来源的代码: Complete Source of Project
<强> 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();
};
此课程的两个最重要的功能是setContentAt
和render
setContentAt
是对象调用以填充&#34;像素&#34;值。为了使它更灵活,我们的类使用指向字符数组的指针而不是直数组(甚至是二维数组)。这允许我们在运行时设置画布的大小。因此,我们使用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
是实际绘制到提示的内容。它所做的就是迭代所有的像素&#34;在它的缓冲区中,将它们放在屏幕上,然后移动到下一行。
// 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来放置&#34;像素&#34;进入缓冲区。
class I_Drawable {
public:
virtual void draw(RenderContext&) = 0;
};
<强> GameBoard
强>
实现I_Drawable的第一个类是GameBoard类,因此能够呈现给我们的RenderContext。这是大多数逻辑的用武之地。它有宽度,高度和整数数组的字段,用于保存电路板上元素的值。它还有两个其他的间距字段。因为当您使用代码绘制电路板时,每个元素之间都有空格。我们不需要将其整合到电路板的底层结构中,我们只需要在绘制时使用它们。
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的原因。总而言之,它遍历棋盘上的每个元素,并在画布上绘制正确的位置,将元素放置在正确的&#34;像素&#34;之下。这是我们合并间距的地方。另外,请注意我们在此函数中渲染光标。
这是一个选择问题,但你可以在GameBoard类之外存储一个标记并在主循环中自己渲染(这将是一个很好的选择,因为它放松了GameBoard类和标记类。然而,由于它们 相当耦合,我选择让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时,我们都会画出一个&#39; =&#39;每个元素之间。如果我们先将光标拉到板上,那么GameBoard会将光标拉过来使其不可见。
如果这是一个更复杂的场景,我们可能不得不做一些奇特的事情,比如使用一个记录元素z-index
的深度缓冲区。然后每当我们绘制时,我们都会检查并查看新元素的z-index是否比RenderContext缓冲区中已有的更接近或更远。根据这一点,我们可能会跳过绘制&#34;像素&#34;共。
我们不是,所以请注意订购您的抽奖电话!
// 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::out
的句柄。 getKeyPress
函数返回用户按下向下的键(忽略键入事件)。 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
强>
您需要担心的最后一件事是如何使用所有这些东西。这是主要进入的地方。你需要一个游戏循环。游戏循环可能是任何游戏中最重要的事情。你看的任何游戏都会有游戏循环。
这个想法是:
这个程序没有什么不同。它首先要做的是创建一个GameBoard和一个RenderContext。它还创建了一个CmdPromptHelper,它允许与命令提示符进行交互。之后,它开始循环并让循环继续,直到我们达到退出条件(对于我们按下逃生)。我们可以有一个单独的类或函数do dispatch输入,但由于我们只是将输入分派给另一个输入处理程序,我将它保存在主循环中。在您获得输入后,您将发送到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);
}
在考虑了所有这些事情后,您就会明白为什么以及在哪里需要绘制光标。这不是一个接一个地调用函数的问题,你需要合成你的绘制。您不能只绘制GameBoard然后绘制标记。至少没有命令提示符。我希望这有帮助。这无疑减轻了工作中的停工时间。