一介のCS人の綴り

トップ > プログラミング > 創作プログラミング
Tweet

今回の主題は?

今回の主題は、Siv3DにおけるFontの機能を中心に簡単なタイピングゲームを自作しよう!ということで、記事を書いていこうかなと思います。

ここでは、このライブラリに慣れるという趣旨で、簡単なプログラムを記述し実行して楽しんでいこう!ということで、「Siv3Dで遊ぼう!」というタイトルで数回記事を書いていきます。

Siv3Dで遊ぼう! - No.2

→ クリックで開閉

グローバル変数の設定

// タイムリミットは10秒
const int Time_Limit = 10;
// 左右内側10は描画しない
const int _Padding = 10;
// 規定のフォント設定
const String _Font = L"font30";

ここでは、タイピングゲームの基本的仕様を記述しております。先に定義しておくことで、後の仕様変更を容易にする目的です。

残り時間に関する関数の設定

今回の作成するタイピングゲームには、解答時間の制限を付けています。仕様上、ミリ秒を基準に経過時間・残り時間を保持しているため、目的に応じて秒単位に変換して処理を行っていたりします。

// ミリ秒を秒に変換した上、小数点第一位までの文字列に変換
inline String makeSecString(int millsec){
	int rest = millsec % 1000; // 残り時間[sec]
	// 小数点第1位が0のときは、省略された0を付加して文字列化
	if (rest <= 50 || rest >= 950)
		return Format(ToString(millsec / 1000.0, 1), L".0");
	else
		return ToString(millsec / 1000.0, 1);
}

Siv3Dにおいて関数ToString()において第2引数で指定した桁数で浮動小数点数を文字列で表すことができるが、末尾が0の場合は省略されてしまいます。
そこで、inline String makeSecString(int millsec)では、取得した残り時間を末尾0を省略せずに文字列化して返すインライン関数となっています。 残り時間を画面上に右揃えで表示するため、末尾が省略されてしまうと秒数の桁の位置の描画がその際だけ一桁分ずれてしまうのを避けるために実装しました。
関数ToString()では、指定した桁より一つ下の桁について値が四捨五入されているため、その四捨五入後、末尾が0となってしまう場合にのみ、末尾に0を付け加えて文字列化しています。

// 残り時間をタイムバーで表示
void timeBar(int remainMillSec){
	// rate = 100 * remainMillSec / (1000 * Time_Limit);
	int rate = remainMillSec / (10 * Time_Limit);
	// 残り時間の割合によってタイムバーの色を変化させる
	Color color = Palette::Green;
	if (rate < 50){
		color = Palette::Yellow;
		if (rate < 20){
			color = Palette::Red;
		}
	}
	// タイムバーの役割を担う長方形を描く
	Rect(Window::Width() * (100.0 - rate) / 100, 65, Window::Width() * rate / 100, 25).draw(color);
}

また、void timeBar(int remainMillSec)では、残り時間を視覚的に表現するため、タイムバーを描画しています。制限時間における残り時間の割合を計算し、 描画するタイムバー(長方形)における左上の頂点の座標を計算し、同時に描画すべき幅をその割合で計算することにより時間につれ右側に縮んでいくタイムバーを実装しました。 さらに、残り時間の割合によってタイムバーの色を変化させています。

文字の配置指定のための関数の設定

// 右揃えでFont表示する
inline void right_font(String o_str, int height, String set_font = _Font, Color color = Palette::White){
	// 表示される文字列の領域を表す長方形の幅を取得
	int o_strWidth = FontAsset(set_font)(o_str).region().w;
	// 「画面の幅 - 文字列の幅 - パディング」を文字列の開始位置として表示
	FontAsset(set_font)(o_str).draw(Window::Width() - o_strWidth - _Padding, height, color);
}

// 左揃えでFont表示する
inline void left_font(String o_str, int height, String set_font = _Font, Color color = Palette::White){
	FontAsset(set_font)(o_str).draw(_Padding, height, color);
}

// 中央揃えでFont表示する
inline void center_font(String o_str, int height, String set_font = _Font, Color color = Palette::White){
	// 表示される文字列の領域を表す長方形の幅を取得
	int o_strWidth = FontAsset(set_font)(o_str).region().w;
	FontAsset(set_font)(o_str).draw(Window::Center().x - o_strWidth / 2, height, color);
}

ここでは、描画する文字列の右揃え・左揃え・中央揃えを実装しています。引数はどの関数も以下のように指定しています。

  • String o_str : どのFontAssetを利用するかを指定する文字列
  • int height : 表示する文字列の位置の高さを指定する整数値
  • String set_font : どのFontAssetを利用するかを指定する文字列。デフォルトで_Font(=L"font30")
  • Color color : 表示する文字列の色の指定。デフォルトでPalette::White

FontAsset(set_font)(o_str).region()により表示する文字列の領域を取得できるため幅を取得し、 画面の幅とグローバル変数で指定した_Padding (= 10)に気をつけて表示させている。

int main(){→ クリックで開閉

ウインドウの設定

    // ウィンドウのタイトルを設定する
	Window::SetTitle(L"Typing Game -siv3D App-");
	// ウィンドウサイズを 幅 640, 高さ 320 にする
	Window::Resize(640, 320);
	// ウインドウの背景として画像を設定
	const Texture texture(L"Example/background.png");
	//プログラム中で用いる文字サイズを設定する
	FontAsset::Register(L"font10", 10);
	FontAsset::Register(L"font20", 20);
	FontAsset::Register(L"font30", 30);
	FontAsset::Register(L"font50", 50);
                        

ここでは、ウインドウの設定を行っています。背景に用いる画像とウインドウサイズを一致させていたり、プログラム中で用いる文字サイズを設定していたりします。 アセット管理を用いて、main関数外でも指定した文字サイズの文字描画ができるようにしています。
ここで用いられているbackground.pngは以下のものを利用しています。(サイトの管理の関係上ファイル名を変更しているため、background.pngに戻してください。)

background.png

問題の入力とタイピングゲームの最終前準備

    Array<String> typing; // タイピング用問題を格納する配列
	// 問題をファイル入力する
	TextReader reader(L"Example/Questions.txt");
	String line;
	while (reader.readLine(line))
	{
		typing.push_back(line);
	}

	int q_num = Random(0, (int)typing.size()); // 問題番号
	String typing_text = L"";
	int combo = 0; // コンボ数

    // ミリ秒で経過時間を取得
	TimerMillisec timerMillisec;
	timerMillisec.start();
                        

タイピング用の問題を一行ずつ入力し、配列に順に格納しています。問題が記述されているQuestions.txtでは、以下のように単に各問題が改行によって区切られています。
このファイルは好きな単語を同様に羅列して作成してもらって問題のバリエーションを増やすことをお勧めします!

abcdefg
aiueo
QWERTY
#include 
Siv3D
Japan
farma_11


また、問題番号を乱数によって取得したり、タイプされた文字列を保持する変数typing_textや、連続正解数を保持する変数comboを宣言したり、 経過時間の取得を開始させたりしています。

問題文等の表示

        texture.draw();	// テクスチャを描く

		// 残り秒数をミリ秒で取得
		int remaining = 1000 * Time_Limit - timerMillisec.elapsed();
		if (remaining <= 0) break;
        // 残り時間の出力
		right_font(Format(makeSecString(remaining), L" Sec"), 90, L"font20", Palette::Gray);
        // タイムバーの描画
		timeBar(remaining);

		// 問題文の出力
		left_font(Format(L"Q.", q_num, L" ", typing[q_num]), 1);
		// コンボ数の出力
		left_font(Format(combo), 290, L"font10", Palette::Green);
                                

ここでは、背景の描画・問題文の表示・残り時間の表示・連続正解数の表示を行っている。 これを行うことで、残りはゲームの中心的機能となるタイピングに移ることができる。

タイピングと正解・不正解

        // text を入力された文字列で更新する
		// バックスペースと改行も有効
		Input::GetCharsHelper(typing_text);
		left_font(Format(L">>> " ,typing_text), 153);

		// まだ入力されていない場合スキップ
		if (typing_text.isEmpty) continue;
		// 現在入力されている文字列が問題と現時点で一致しているかどうか
		if (typing[q_num].startsWith(typing_text)){
			left_font(L"OK!", 230, L"font30", Palette::Green);
			// 正解した場合
			if (typing[q_num] == typing_text){
				right_font(L"CLEAR!!!", 215, L"font50", Palette::Green);
				combo++;
				WaitKey();

				typing_text = L"";
				q_num = Random(0, (int)typing.size());
				timerMillisec.restart(); // 0 にリセットして開始
			}
		}
		else {
			left_font(L"Miss...", 230, L"font30", Palette::Red);
		}
                                

Input::GetCharsHelper(typing_text)によって、タイピングされた文字が変数typing_textに順々に入力される。 同時に、現在入力されている文字列を左揃えで表示している。

また、現状の変数typing_textについて問題文と一致しているかで正解・不正解を画面上に反映させる。 関数startsWith()が、引数に指定した文字列から始まるかどうかの真偽を返してくれるため、 問題文が現在入力されている文字列から始まったいるのなら"OK!"を、いないなら"Miss..."を表示させている。
さらに、問題文と入力された文字列が完全に一致した場合、この問題に正解したとし、この問題に用いられた各々の変数を初期化したうえで、 次の問題に移る処理をしている。 ただし、制限時間以内に正解と判断されない場合、タイピングゲームにおけるメインループから外され、強制的にゲームが終了する。

←} // while (System::Update())

ゲームオーバー画面の描画

        center_font(L"--- Game Over ---", Window::Center().y - 100, L"font50", Palette::Red);
		center_font(L"Push Any Button", Window::Center().y, L"font20", Palette::Red);

		// 何かキーが押されると終了する
		if (Input::AnyKeyClicked()) break;
                                

ここでは、タイピングを制限以内に正解できなかった時に表示させるゲームオーバー画面を描画している。
また、何かキーを入力するとゲームが完全に終了することとなる。

←} // while (System::Update())
←} // int main()

ソースコードの全容

# include <siv3d.hpp>

const int Time_Limit = 10;
const int _Padding = 10;
const String _Font = L"font30";

// ミリ秒を秒に変換した上、小数点第一位までの文字列に変換
inline String makeSecString(int millsec){
	int rest = millsec % 1000; // 残り時間[sec]
	if (rest <= 50 || rest >= 950)
		return Format(ToString(millsec / 1000.0, 1), L".0");
	else
		return ToString(millsec / 1000.0, 1);
}

// 右揃えでFont表示する
inline void right_font(String o_str, int height, String set_font = _Font, Color color = Palette::White){
	int o_strWidth = FontAsset(set_font)(o_str).region().w;
	FontAsset(set_font)(o_str).draw(Window::Width() - o_strWidth - _Padding, height, color);
}

// 左揃えでFont表示する
inline void left_font(String o_str, int height, String set_font = _Font, Color color = Palette::White){
	FontAsset(set_font)(o_str).draw(_Padding, height, color);
}

// 中央揃えでFont表示する
inline void center_font(String o_str, int height, String set_font = _Font, Color color = Palette::White){
	int o_strWidth = FontAsset(set_font)(o_str).region().w;
	FontAsset(set_font)(o_str).draw(Window::Center().x - o_strWidth / 2, height, color);
}

// 残り時間をタイムバーで表示
void timeBar(int remainMillSec){
	int rate = remainMillSec / (10 * Time_Limit);
	Color color = Palette::Green;
	if (rate < 50){
		color = Palette::Yellow;
		if (rate < 20){
			color = Palette::Red;
		}
	}
	Rect(Window::Width() * (100.0 - rate) / 100, 65, Window::Width() * rate / 100, 25).draw(color);
}

void Main()
{
	Window::SetTitle(L"Typing Game -siv3D App-");
	Window::Resize(640, 320);
	const Texture texture(L"Example/background.png");
	FontAsset::Register(L"font10", 10);
	FontAsset::Register(L"font20", 20);
	FontAsset::Register(L"font30", 30);
	FontAsset::Register(L"font50", 50);
	Array<String> typing;

	TextReader reader(L"Example/Questions.txt");
	String line;
	while (reader.readLine(line))
	{
		typing.push_back(line);
	}

	TimerMillisec timerMillisec;
	timerMillisec.start();
	int q_num = Random(0, (int)typing.size());
	String typing_text = L"";
	int combo = 0;

	while (System::Update())
	{
		texture.draw();
		int remaining = 1000 * Time_Limit - timerMillisec.elapsed();
		if (remaining <= 0) break;
        timeBar(remaining);
		right_font(Format(makeSecString(remaining), L" Sec"), 90, L"font20", Palette::Gray);

		left_font(Format(L"Q.", q_num, L" ", typing[q_num]), 1);
		left_font(Format(combo), 290, L"font10", Palette::Green);

		Input::GetCharsHelper(typing_text);
		left_font(Format(L">>> " ,typing_text), 153);

		if (typing_text.isEmpty) continue;
		if (typing[q_num].startsWith(typing_text)){
			left_font(L"OK!", 230, L"font30", Palette::Green);

			if (typing[q_num] == typing_text){
				right_font(L"CLEAR!!!", 215, L"font50", Palette::Green);
				combo++;
				WaitKey();
				typing_text = L"";
				q_num = Random(0, (int)typing.size());
				timerMillisec.restart(); // 0 にリセットして開始
			}
		}
		else {
			left_font(L"Miss...", 230, L"font30", Palette::Red);
		}
	}

	while (System::Update())
	{
		center_font(L"--- Game Over ---", Window::Center().y - 100, L"font50", Palette::Red);
		center_font(L"Push Any Button", Window::Center().y, L"font20", Palette::Red);
		if (Input::AnyKeyClicked()) break;
	}
}
        

実行結果

実行結果1 実行結果2 実行結果3 実行結果4

Comments

Top