ChatGptを使用してC++のコンソール用ヘッダーを作る①

ゲーム制作のため先に描画部分以外のコードを作成し、コンソール上で実行しようと思ったため作成しました。
私はChatGptをアップグレードしていないためGPT-4でなくGPT-3.5を使用してこの記事のコードを作成しています。その際に困ったことや自分で修正しなくてはいけなかった箇所を書いていけたらいいなと思う。


作成したヘッダーの機能

今回私がChatGptに作成してもらった機能は以下である
・コンストラクターで画面のサイズを指定できる
・小文字を大文字に変更できる(現在修正中)
・バッファーを二つ作成し、その中に描画する文字を作成する
・文字には一文字ずつ色を付けられる
・ある点を中心に文字列を描画できる
・文字の描画、バッファーの入れ替えは前のフレームから中身が変更のあった時だけにする

ChatGptを使用してよかった点

ChatGptを使用してよかった点は、自分に知識がない分野のコードを作成してくれることです。私はC++を勉強し始めてまだ1年ほどでありWindows.hなどのヘッダーを使用した事がない。なので画面サイズの変更や、コンソール上にマウスカーソルが出ないようにする、画面サイズの変更などといったコードを記述することが出来ない。なのでここら辺のコードを気軽に作ってくれるのはありがたかった。またこれらのコードをより最適化したいと考えたときに質問することが出来る点もよかった。

ChatGptの残念だった点

ChatGptを使用していて残念だった点は頑固な点である。生成されたコードを動かしてエラーが出た時など「エラーが出ました」などというと修正してくれる。しかし、その後会話をしているといつの間にか修正前のコードに戻されていたりする。また、修正できませんや、解決できませんといった選択肢がないためどんどんエラーが深刻化していくことがある。その際には新たにChatを開始しなければならなかった。また勝手に追加してくることが多く、画面描画時などにはいらない場所に「<<endl」を追加して一段ずつ上に行くといったこともあった。

最適化した箇所

最適化しなければならなかった点はかなり多かった。ChatGptのコードは最適化がされておらず、動きはするがかなりもっさりしていて画面がちらつくなどしていた。元々ChatGptが作成したコードではWindowsの機能で色を変更してからChar型の文字を表示していた。この方法ではコンソールに移すのに時間がかかるだけでなく処理の無駄が多かった。

最適化前
if (bufferChanged) {
		consoleClear();
		for (int i = 0; i < consoleHeight; ++i) {
			for (int j = 0; j < consoleWidth; ++j) {
				ConsoleCell& cell = (*currentBuffer)[i][j];
				SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), cell.textColor);
				cout << cell.character;
			}
			if (i != consoleHeight - 1) {	//最終行でカーソルが改行されると文字が一段上になるから
				cout << endl;
			}
		}
		currentBuffer = (currentBuffer == &buffer1) ? &buffer2 : &buffer1;
	}

そこで私の学校の教師にコンソールの限界ですか?と聞いたところ、文字ごとに変えるのではなくエスケープシーケンスを使用して表示すればと金言をもらうことが出来た。そこでいかのように修正した。※Windowsとエスケープシーケンスで表示できる色が異なるため「P」の色が異なります。

最適化後
if (bufferChanged) {
		std::string outputBuffer = ""; // 文字列を溜め込むバッファ
		// バッファ1とバッファ2を比較し、変更がある場合に文字列に変換して溜め込む
		for (int i = 0; i < consoleHeight; ++i) {
			for (int j = 0; j < consoleWidth; ++j) {
				ConsoleCell& cell = (*currentBuffer)[i][j];
				ColorEnum textColor = static_cast<ColorEnum>(cell.textColor);
				std::string colorCode = ColorConverter::colorToEscapeSequence(textColor);
				std::string resetCode = ColorConverter::resetEscapeSequence();
				if (j != 0) {	//同じ行の一個前のと比較
					if ((*currentBuffer)[i][j - 1].textColor == (*currentBuffer)[i][j].textColor) {
						outputBuffer += cell.character;
					}
					else {
						if (colorCode == "") {
							outputBuffer += resetCode + cell.character;
						}
						else {
							outputBuffer += colorCode + cell.character;
						}
					}
				}
				else {
					if ((i != 0) && ((*currentBuffer)[i - 1][consoleWidth - 1].textColor == (*currentBuffer)[i][j].textColor)) {
						outputBuffer += cell.character;
					}
					else {
						if (colorCode == "") {
							outputBuffer += resetCode + cell.character;
						}
						else {
							outputBuffer += colorCode + cell.character;
						}
					}
				}
			}
			if (i != consoleHeight - 1) {
				outputBuffer += '\n'; // 改行を追加(最終行でない場合)
			}
		}
		consoleClear();
		// コンソールに一括で出力
		std::cout << outputBuffer;

		// バッファを交換
		currentBuffer = (currentBuffer == &buffer1) ? &buffer2 : &buffer1;
	}

すべての文字を一つのstring型の文字列に入れ、改行コードや色コードもここに入れるようにしました。
実行環境によってはちらつくこともありますが、画面クリアの残像は見えなくなりました。

今のところのコード

エラー処理などをしていないため使用は自己責任でお願いします。

 #ifndef  CONSOLEUTILS_H #define  CONSOLEUTILS_H

// ConsoleUtils クラスの定義
 #include  <iostream> #include  <windows.h> #include  <string> #include  <vector>

using namespace std;

/**************************************************************************
*2023_9_23  Kousuke Suzuki                        *
**************************************************************************/

enum class ColorEnum {
	RED,
	GREEN,
	YELLOW,
	BLUE,
	MAGENTA,
	CYAN,
	WHITE
};

// ColorEnum から文字列に変換するクラス ColorConverter
class ColorConverter {
public:
	static std::string colorToEscapeSequence(ColorEnum color) {
		static std::map<ColorEnum, std::string> colorMap = {
			{ColorEnum::RED, "\x1b[31m"},
			{ColorEnum::GREEN, "\x1b[32m"},
			{ColorEnum::YELLOW, "\x1b[33m"},
			{ColorEnum::BLUE, "\x1b[34m"},
			{ColorEnum::MAGENTA, "\x1b[35m"},
			{ColorEnum::CYAN, "\x1b[36m"},
			{ColorEnum::WHITE, "\x1b[37m"}
		};

		auto it = colorMap.find(color);
		if (it != colorMap.end()) {
			return it->second;
		}

		return ""; // 不明な色の場合は空文字列を返す
	}

	static std::string resetEscapeSequence() {
		return "\x1b[0m";
	}
};

class ConsoleUtils {
public:
	// コンストラクタ
	ConsoleUtils(int width, int height, int defaultColor = 7);

	// 画面サイズを変更する関数
	void setConsoleSize(int width, int height);

	// バッファをクリアする関数
	void clearBuffer();
	void clearBackBuffer();

	// 文字列を半角から全角に変換する関数
	string convertToFullWidth(const string& text);
	string convertToFullWidth(const char character);
	bool isFullWidth(const char character);

	// バッファに文字列を描画する関数
	void writeToBuffer(int x, int y, const string& text, int textColor = 7);
	void writeToBuffer(int x, int y, char character, int textColor = 7);

	// 中央に文字列を描画する関数
	void writeToCenter(int x, int y, const string& text, int textColor = 7);

	// バッファをコンソールに表示する関数
	void renderToConsole();
	void consoleClear();

private:
	// コンソールセルを表す構造体
	struct ConsoleCell {
		char character;
		int textColor;
	};

	// コンソールの幅と高さ
	int consoleWidth;
	int consoleHeight;

	// テキストカラー
	int textColor;

	// ダブルバッファ
	std::vector<std::vector<ConsoleCell>> buffer1;
	std::vector<std::vector<ConsoleCell>> buffer2;

	// 現在のバッファへのポインタ
	std::vector<std::vector<ConsoleCell>>* currentBuffer;
};

ConsoleUtils::ConsoleUtils(int width, int height, int defaultColor)
	: consoleWidth(width), consoleHeight(height), textColor(defaultColor) {
	buffer1.resize(consoleHeight, vector<ConsoleCell>(consoleWidth));
	buffer2.resize(consoleHeight, vector<ConsoleCell>(consoleWidth));
	currentBuffer = &buffer1;
	clearBuffer();
	setConsoleSize(width, height);

	// カーソルを非表示にする
	CONSOLE_CURSOR_INFO cursorInfo;
	cursorInfo.dwSize = 1;
	cursorInfo.bVisible = false;
	SetConsoleCursorInfo(GetStdHandle(STD_OUTPUT_HANDLE), &cursorInfo);
	//拡大ボタンをなくす
	HWND consoleWindow = GetConsoleWindow();

	if (consoleWindow != NULL) {
		// システムメニューを取得
		HMENU systemMenu = GetSystemMenu(consoleWindow, FALSE);

		if (systemMenu != NULL) {
			// SC_MAXIMIZEを無効にする
			EnableMenuItem(systemMenu, SC_MAXIMIZE, MF_GRAYED);
		}
	}

	// コンソールウィンドウをリサイズできないようにする
	LONG style = GetWindowLong(consoleWindow, GWL_STYLE);
	style &= ~WS_MAXIMIZEBOX; // WS_MAXIMIZEBOXをクリア
	SetWindowLong(consoleWindow, GWL_STYLE, style);
    //コンソール画面にタイトルをつける
	const wchar_t* newTitle = L"A"; // wchar_t型の文字列を使用

	// ANSI文字列をSetConsoleTitleに渡す
	SetConsoleTitle(newTitle);
}

void ConsoleUtils::setConsoleSize(int width, int height) {
	consoleWidth = width;
	consoleHeight = height;
	buffer1.resize(consoleHeight, vector<ConsoleCell>(consoleWidth));
	buffer2.resize(consoleHeight, vector<ConsoleCell>(consoleWidth));
	clearBuffer();
	HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE);
	SMALL_RECT rect = { 0, 0, static_cast<SHORT>(consoleWidth - 1), static_cast<SHORT>(consoleHeight - 1) };
	SetConsoleWindowInfo(hConsole, TRUE, &rect);
	COORD coord = { static_cast<SHORT>(consoleWidth), static_cast<SHORT>(consoleHeight) };
	SetConsoleScreenBufferSize(hConsole, coord);
}

void ConsoleUtils::clearBuffer() {
	for (int i = 0; i < consoleHeight; ++i) {
		for (int j = 0; j < consoleWidth; ++j) {
			buffer1[i][j].character = ' ';
			buffer1[i][j].textColor = textColor;
			buffer2[i][j].character = ' ';
			buffer2[i][j].textColor = textColor;
		}
	}
}

void ConsoleUtils::clearBackBuffer() {
	for (int i = 0; i < consoleHeight; ++i) {
		for (int j = 0; j < consoleWidth; ++j) {
			(*currentBuffer)[i][j].character = ' ';
			(*currentBuffer)[i][j].textColor = textColor;
		}
	}
}

// 文字を半角から全角に変換する関数
string ConsoleUtils::convertToFullWidth(const char character) {
	 //全角文字かどうかを判定
	if (isFullWidth(character)) {
		// 全角文字の場合は変換せずにそのまま返す
		return string(1, character);
	}

	// 半角スペースから半角チルダまでの文字を全角に変換
	if (character >= ' ' && character <= '~') {
		return string(1, static_cast<char>(character + 0xFEE0));
	}
	else {
		// それ以外の文字はそのまま追加
		return string(1, character);
	}
	return string(1,character);
}


// 文字列を半角から全角に変換する関数
string ConsoleUtils::convertToFullWidth(const string& text) {
	string fullWidthText;
	for (char c : text) {
		// 全角文字かどうかを判定
		if (isFullWidth(c)) {
			// 全角文字の場合は変換せずにそのまま返す
			fullWidthText += c;
		}
		else if (c >= ' ' && c <= '~') {
			// 半角スペースから半角チルダまでの文字を全角に変換
			fullWidthText += static_cast<char>(c + 0xFEE0);
		}
		else {
			// それ以外の文字はそのまま追加
			fullWidthText += c;
		}
	}
	return fullWidthText;
}

// 文字が全角文字かどうかを判定する関数
bool ConsoleUtils::isFullWidth(const char character) {
	// 全角文字の文字コードの範囲をチェック
	return (character >= 58 && character <= 96) || (character >= 33 && character <= 47);
}

void ConsoleUtils::writeToBuffer(int x, int y, const string& text, int textColor) {
	//string convertedText = convertToFullWidth(text);
	string convertedText = text;
	for (size_t i = 0; i < convertedText.size(); ++i) {
		if (x + i >= 0 && x + i < consoleWidth && y >= 0 && y < consoleHeight) {
			(*currentBuffer)[y][x + i].character = convertedText[i];
			(*currentBuffer)[y][x + i].textColor = textColor;
		}
	}
}

void ConsoleUtils::writeToBuffer(int x, int y, char character, int textColor) {
	//string convertedCharacter = convertToFullWidth(character);
	string convertedCharacter = string(1, character);
	if (character == ' '||character==NULL) {
		convertedCharacter = ' ';
	}
	if (x >= 0 && x < consoleWidth && y >= 0 && y < consoleHeight) {
		(*currentBuffer)[y][x].character = convertedCharacter[0];
		(*currentBuffer)[y][x].textColor = textColor;
	}
}

void ConsoleUtils::writeToCenter(int x, int y, const string& text, int textColor) {
	int textWidth = static_cast<int>(text.size());
	int startX = x - textWidth / 2;
	writeToBuffer(startX, y, text, textColor);
}

void ConsoleUtils::renderToConsole() {
	bool bufferChanged = false; // バッファの変更があるかどうかを示すフラグ

	// バッファ1とバッファ2を比較
	for (int i = 0; i < consoleHeight; ++i) {
		for (int j = 0; j < consoleWidth; ++j) {
			if (buffer1[i][j].character != buffer2[i][j].character ||
				buffer1[i][j].textColor != buffer2[i][j].textColor) {
				bufferChanged = true;
				break; // 変更があったらループを抜ける
			}
		}
		if (bufferChanged) {
			break; // 変更があったらループを抜ける
		}
	} #if  0
	if (bufferChanged) {	//描画速度を上げるなら色指定をしないでstringで一列ずつ表示したほうが良い
		consoleClear();
		for (int i = 0; i < consoleHeight; ++i) {
			for (int j = 0; j < consoleWidth; ++j) {
				ConsoleCell& cell = (*currentBuffer)[i][j];
				SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), cell.textColor);
				cout << cell.character;
			}
			if (i != consoleHeight - 1) {	//最終行でカーソルが改行されると文字が一段上になるから
				cout << endl;
			}
		}
		currentBuffer = (currentBuffer == &buffer1) ? &buffer2 : &buffer1;
	} #endif  #if  1
	if (bufferChanged) {
		std::string outputBuffer = ""; // 文字列を溜め込むバッファ
		// バッファ1とバッファ2を比較し、変更がある場合に文字列に変換して溜め込む
		for (int i = 0; i < consoleHeight; ++i) {
			for (int j = 0; j < consoleWidth; ++j) {
				ConsoleCell& cell = (*currentBuffer)[i][j];
				ColorEnum textColor = static_cast<ColorEnum>(cell.textColor);
				std::string colorCode = ColorConverter::colorToEscapeSequence(textColor);
				std::string resetCode = ColorConverter::resetEscapeSequence();
				if (j != 0) {	//同じ行の一個前のと比較
					if ((*currentBuffer)[i][j - 1].textColor == (*currentBuffer)[i][j].textColor) {
						outputBuffer += cell.character;
					}
					else {
						if (colorCode == "") {
							outputBuffer += resetCode + cell.character;
						}
						else {
							outputBuffer += colorCode + cell.character;
						}
					}
				}
				else {
					if ((i != 0) && ((*currentBuffer)[i - 1][consoleWidth - 1].textColor == (*currentBuffer)[i][j].textColor)) {
						outputBuffer += cell.character;
					}
					else {
						if (colorCode == "") {
							outputBuffer += resetCode + cell.character;
						}
						else {
							outputBuffer += colorCode + cell.character;
						}
					}
				}
			}
			if (i != consoleHeight - 1) {
				outputBuffer += '\n'; // 改行を追加(最終行でない場合)
			}
		}
		consoleClear();
		// コンソールに一括で出力
		std::cout << outputBuffer;

		// バッファを交換
		currentBuffer = (currentBuffer == &buffer1) ? &buffer2 : &buffer1;
	} #endif 
}

void ConsoleUtils::consoleClear() { #if  1
	COORD topLeft = { 0, 0 };
	DWORD written;
	HANDLE console = GetStdHandle(STD_OUTPUT_HANDLE);

	// バッファを消去
	FillConsoleOutputCharacter(console, ' ', consoleWidth * consoleHeight, topLeft, &written);
	FillConsoleOutputAttribute(console, textColor, consoleWidth * consoleHeight, topLeft, &written);

	// カーソルを画面の左上に移動
	SetConsoleCursorPosition(console, topLeft); #endif  #if  0
	system("cls"); #endif 
}
 #endif  // CONSOLEUTILS_H

この記事が気に入ったらサポートをしてみませんか?