あなたがこれをしている方法で永続的な描画はありません。シェル/コマンドプロンプトに直接印刷しているだけです。あなたがやろうとしている方法はうまくいきません。描画後にプロンプトに描画されたものを編集することはできません。基本的に画面をクリアしてから再度描画する必要がありますが、指定されたメーカーを使用します。
あなたが課題でライブラリを使用できるかどうかはわかりませんが、使用できる非常に優れたライブラリはncursesです
編集 回答の完全な書き直し
CMD で物を重ねて描画する
仕事でダウンタイムがあったので、必要なことを行うためのプロジェクトを書きました。コードを投稿して、その機能と途中で必要な理由を説明します。
基本的にレンダー バッファーまたはレンダー コンテキストが必要になることをまず考えてください。OpenGL などのグラフィックス API でプログラミングするときは常に、画面に直接レンダリングするだけでなく、コンテンツをラスタライズしてピクセルに変換するバッファに各オブジェクトをレンダリングします。そのフォームになると、API はレンダリングされた画像を画面に押し込みます。GPU のピクセル バッファーに描画する代わりに、文字バッファーに描画する同様のアプローチを使用します。各文字を画面上のピクセルと考えてください。
完全なソースのペーストビンは次のとおりです:
プロジェクトの完全なソース
RenderContext
これを行うクラスがクラスになりますRenderContext
。幅と高さを保持するフィールドとchars
、バッファをクリアするたびにバッファを埋める特別な char の配列があります。
このクラスは単に配列と関数を保持して、レンダリングできるようにします。それに引き寄せるときに、境界内にいることを確認します。オブジェクトがクリッピング スペースの外側 (画面外) に描画しようとする可能性があります。ただし、そこに描かれたものはすべて破棄されます。
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();
};
このクラスの 2 つの最も重要な機能はsetContentAt
、 とrender
setContentAt
「ピクセル」値を埋めるためにオブジェクトが呼び出すものです。これをもう少し柔軟にするために、このクラスでは、単純な配列 (または 2 次元配列) ではなく、char の配列へのポインターを使用します。これにより、実行時にキャンバスのサイズを設定できます。このため、この配列の要素にアクセスし、次のx + (y * m_width)
ような 2 次元の逆参照を置き換えます。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 クラスです。これは、ロジックの大部分の出番です。これには、幅、高さ、およびボード上の要素の値を保持する整数配列のフィールドがあります。また、間隔のための 2 つのフィールドもあります。コードを使用してボードを描画すると、各要素の間にスペースができます。これをボードの基本構造に組み込む必要はありません。描画するときにそれらを使用するだけです。
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 の子ノードになるため、次のようになります。ただし、明示的なマーカーを 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 に 4 つのシンボルを配置するだけで、ボード上で選択した要素の輪郭を描くことができます。Marker は 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 を使用している場合は、シェルへの描画を自分で処理することについて心配する必要があります。
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
最後に心配する必要があるのは、これらすべてをどのように使用するかです。そこでメインの出番です。ゲームループが必要です。ゲーム ループは、おそらくどのゲームでも最も重要なことです。あなたが見ているどのゲームにもゲームループがあります。
アイデアは次のとおりです。
- 画面に何かを表示する
- 入力の読み取り
- 入力を処理する
- 後藤1
このプログラムも例外ではありません。最初に、GameBoard と RenderContext を作成します。また、コマンド プロンプトとのインターフェイスを可能にする CmdPromptHelper も作成します。その後、ループを開始し、終了条件に達するまでループを継続させます (ここではエスケープを押しています)。別のクラスまたは関数で入力をディスパッチすることもできますが、入力を別の入力ハンドラーにディスパッチするだけなので、メイン ループに保持しました。入力を取得したら、それに応じて自身を変更する GameBoard に if off を送信します。次のステップは、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 を描画してからマーカーを描画することはできません。少なくともコマンドプロンプトではありません。これが役立つことを願っています。仕事のダウンタイムが確実に減りました。