アマゾンバナーリンク

.Net Frameworkでのタイピングゲームの作り方

2019年12月27日

前回の記事でDXライブラリの使い方とオブジェクト指向の基本である、継承、多様性についてお話しました。 そこで本来ならば、アクション、シューティングなど動きがあって、DXライブラリの描画機能をフルにいかせるようなゲームの作り方を紹介するか迷いましたが、今回はタイピングゲームの作り方を説明します。全部で3記事構成となりますが、すべてローマ字入力に対しての打ち分けの処理やかな入力にも対応しているので、本格的なタイピングゲームを作りたい人には役に立つと思います。

この記事を書く目的

DXライブラリを使えば、TODのようなゲームを作ることも可能です。
しかし、今回はあえて使わないで、タイピングの根幹の機能の作る紹介をします。
なぜかというと、考え方を真似して他の環境でも、作るのが簡単だからです。
私自身競技タイピングをしてくれる人が増えてほしいし、これを機にタイピングゲームを作ってくれる人が増える事を願ってます。

タイピングゲームの特性

ゲームは通常、毎ループ処理が必要で特にFPSなどが気になる事が多いですよね?
しかし、タイピングゲームは物にもよりますが、基本的にキー入力があってから、処理をするというイベントドリブン型になります。

タイピングゲームを作るのに必要な処理

  • 問題文章の読み込み
  • どのキーを入力したか取得
  • 文字と押したキーがあっているか判定
  • 正解した時に文字を進める
  • ミスした時の処理
  • はみ出した文字をスクロールさせる機能

これに加えてローマ字入力の時には

  • かな文字をローマ字に変換する
  • 複数のローマ字の打ち方に対応する

という処理があるので、これが意外と大変です。

実際にタイピングゲームを作る

それではさっそく一番簡単なタイピングゲームを作ってみます。
Windowsフォームアプリケーション(.NET Framework)
プロジェクト名はTypingGameとする。まったくの初心者の方は下の初心者講座を参考にしてください。

リッチテキストボックスに入力した文字を表示させる

以下に手順を示します。まずはForm1.cs[デザイナ]で行います。

  • 上記の写真のようにツールボックスからRichTextBoxを2つ、RadioButtonを2つ、ボタンを1つ配置
  • 中身を入力できないように richTextBox1とrichTextBox2のプロパティのReadOnlyをTrue
  • ツールボックスからボタンを1つ配置してプロパティのTextをスタートという名前にする
  • richTextBox1とrichTextBox2のScrollBarsをNoneにする

次にRichTextBoxにフォーカスがいかないようにrichTextBox1のプロパティの雷マークをクリックしてEnter関数をダブルクリックする。
そしたら、Form1.csに下のコードが追加されるのでbutton1.Focus();を追加する。

private void richTextBox1_Enter(object sender, EventArgs e)
{
	button1.Focus(); // この部分を追加
}

richTextBox2にも上と同じ作業をする。
最終的には以下の様にコードを書きます。

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace TypingGame
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }
        // 最初に1度だけ呼ばれる
        private void Form1_Load(object sender, EventArgs e)
        {
            // 問題文のフォントサイズ
            int rich_text_font_size = 16;
            richTextBox1.Font = new Font(System.Windows.Forms.Control.DefaultFont.FontFamily, rich_text_font_size, FontStyle.Bold, GraphicsUnit.Pixel);
            richTextBox2.Font = new Font(System.Windows.Forms.Control.DefaultFont.FontFamily, rich_text_font_size, FontStyle.Bold, GraphicsUnit.Pixel);
        }
        // キー入力がある度に呼ばれる
        protected override void OnKeyDown(KeyEventArgs e)
        {   
            // 文字を分解する時に空文字は取り除く様にオプションを指定
            System.StringSplitOptions option = System.StringSplitOptions.RemoveEmptyEntries;
            // 「,」を区切り文字にstring型の配列linesに文字を分解して代入する
            string[] lines = e.KeyData.ToString().Split(new char[] { ',' }, option);
            richTextBox1.Text += lines[0];       
        }
        // リッチテキストにフォーカスが言った時
        private void richTextBox1_Enter(object sender, EventArgs e)
        {
            button1.Focus();
        }

        private void richTextBox2_Enter(object sender, EventArgs e)
        {
            button1.Focus();
        }
        // ボタンがクリックされたら
        private void button1_Click_1(object sender, EventArgs e)
        {
            // どのフォーカスでも入力を受け付けるようにする
            KeyPreview = !KeyPreview;
        }
    }
}

実行してボタンを一度押すとリッチテキストボックスに入力したキーコードが表示されます。次に問題文を表示して、キー入力した文字を一文字ずつ消して、全部消したら次の文章に行く機能を実装します。

最低限のタイピングゲームの機能を実装する

ソリューションエクスプローラーのプロジェクトを右クリックしてからクラスをCTyping.csと名前を付けて追加します。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace TypingGame
{
    public class CTyping
    {   // 上の段の問題文章
        List<string> ViewText = new List<string>() { "typing","test", "option"};
        // 下の段の問題文章
        List<string> InputTextk = new List<string>() { "typing","test" , "option" };
        string[,] Cw = new string[48, 3];
        int TextPos = 0; // 何問目の問題かを示す
        int StrPosA = 0; // View文字が何文字目かを示す
        int StrPosE = 0; // Input文字が何文字目かを示す
        RichTextBox ViewRichTextBox; // 上の段の問題を表示するリッチテキストボックス
        RichTextBox InputRichTextBox; // 下の段の問題を表示するリッチテキストボックス
        // コンストラクタ(インスタンス化された時に一度だけ呼ばれる)
        public CTyping(RichTextBox view_rich_text, RichTextBox input_rich_text)
        {
            ViewRichTextBox = view_rich_text;
            InputRichTextBox = input_rich_text;
            SetChangeWord();
        }
        // ボタンをクリックしたら呼ばれる
        public void GameStart()
        {
            TextPos = StrPosA = StrPosE = 0;
            ViewRichTextBox.Text = ViewText[TextPos];
            InputRichTextBox.Text = InputTextk[TextPos];
        }
        // かなロックを使わないで済ますためにこの関数でキーコードを変換する
        void SetChangeWord()
        {
            // キーボード1行目
            Cw[0, 0] = "D1"; Cw[0, 1] = "1"; Cw[0, 2] = "!";
            Cw[1, 0] = "D2"; Cw[1, 1] = "2"; Cw[1, 2] = "\"";
            Cw[2, 0] = "D3"; Cw[2, 1] = "3"; Cw[2, 2] = "#";
            Cw[3, 0] = "D4"; Cw[3, 1] = "4"; Cw[3, 2] = "$";
            Cw[4, 0] = "D5"; Cw[4, 1] = "5"; Cw[4, 2] = "%";
            Cw[5, 0] = "D6"; Cw[5, 1] = "6"; Cw[5, 2] = "&";
            Cw[6, 0] = "D7"; Cw[6, 1] = "7"; Cw[6, 2] = "'";
            Cw[7, 0] = "D8"; Cw[7, 1] = "8"; Cw[7, 2] = "(";
            Cw[8, 0] = "D9"; Cw[8, 1] = "9"; Cw[8, 2] = ")";
            Cw[9, 0] = "D0"; Cw[9, 1] = "0"; Cw[9, 2] = "ヲ";
            Cw[10, 0] = "OemMinus"; Cw[10, 1] = "-"; Cw[10, 2] = "=";
            Cw[11, 0] = "Oem7"; Cw[11, 1] = "^"; Cw[11, 2] = "~";
            Cw[12, 0] = "Oem5"; Cw[12, 1] = "\\"; Cw[12, 2] = "|";

            // キーボード2行目
            Cw[13, 0] = "Q"; Cw[13, 1] = "q"; Cw[13, 2] = "Q";
            Cw[14, 0] = "W"; Cw[14, 1] = "w"; Cw[14, 2] = "W";
            Cw[15, 0] = "E"; Cw[15, 1] = "e"; Cw[15, 2] = "E";
            Cw[16, 0] = "R"; Cw[16, 1] = "r"; Cw[16, 2] = "R";
            Cw[17, 0] = "T"; Cw[17, 1] = "t"; Cw[17, 2] = "T";
            Cw[18, 0] = "Y"; Cw[18, 1] = "y"; Cw[18, 2] = "Y";
            Cw[19, 0] = "U"; Cw[19, 1] = "u"; Cw[19, 2] = "U";
            Cw[20, 0] = "I"; Cw[20, 1] = "i"; Cw[20, 2] = "I";
            Cw[21, 0] = "O"; Cw[21, 1] = "o"; Cw[21, 2] = "O";
            Cw[22, 0] = "P"; Cw[22, 1] = "p"; Cw[22, 2] = "P";
            Cw[23, 0] = "Oemtilde"; Cw[23, 1] = "@"; Cw[23, 2] = "`";
            Cw[24, 0] = "OemOpenBrackets"; Cw[24, 1] = "["; Cw[24, 2] = "{";

            // キーボード3行目
            Cw[25, 0] = "A"; Cw[25, 1] = "a"; Cw[25, 2] = "A";
            Cw[26, 0] = "S"; Cw[26, 1] = "s"; Cw[26, 2] = "S";
            Cw[27, 0] = "D"; Cw[27, 1] = "d"; Cw[27, 2] = "D";
            Cw[28, 0] = "F"; Cw[28, 1] = "f"; Cw[28, 2] = "F";
            Cw[29, 0] = "G"; Cw[29, 1] = "g"; Cw[29, 2] = "G";
            Cw[30, 0] = "H"; Cw[30, 1] = "h"; Cw[30, 2] = "H";
            Cw[31, 0] = "J"; Cw[31, 1] = "j"; Cw[31, 2] = "J";
            Cw[32, 0] = "K"; Cw[32, 1] = "k"; Cw[32, 2] = "K";
            Cw[33, 0] = "L"; Cw[33, 1] = "l"; Cw[33, 2] = "L";
            Cw[34, 0] = "Oemplus"; Cw[34, 1] = ";"; Cw[34, 2] = "+";
            Cw[35, 0] = "Oem1"; Cw[35, 1] = ":"; Cw[35, 2] = "*";
            Cw[36, 0] = "Oem6"; Cw[36, 1] = "]"; Cw[36, 2] = "}";

            // キーボード4行目
            Cw[37, 0] = "Z"; Cw[37, 1] = "z"; Cw[37, 2] = "Z";
            Cw[38, 0] = "X"; Cw[38, 1] = "x"; Cw[38, 2] = "X";
            Cw[39, 0] = "C"; Cw[39, 1] = "c"; Cw[39, 2] = "C";
            Cw[40, 0] = "V"; Cw[40, 1] = "v"; Cw[40, 2] = "V";
            Cw[41, 0] = "B"; Cw[41, 1] = "b"; Cw[41, 2] = "B";
            Cw[42, 0] = "N"; Cw[42, 1] = "n"; Cw[42, 2] = "N";
            Cw[43, 0] = "M"; Cw[43, 1] = "m"; Cw[43, 2] = "M";
            Cw[44, 0] = "Oemcomma"; Cw[44, 1] = "、"; Cw[44, 2] = "<";
            Cw[45, 0] = "OemPeriod"; Cw[45, 1] = "。"; Cw[45, 2] = ">";
            Cw[46, 0] = "OemQuestion"; Cw[46, 1] = "/"; Cw[46, 2] = "?";
            Cw[47, 0] = "OemBackslash"; Cw[47, 1] = "\\"; Cw[47, 2] = "_";
        }
        // キーコードをわかりやすい文字に変換して返す
        public  string GetKeyString(string input_str, bool shift, bool showkeypress_mode = false)
        {
            // ShowKeyPressで使う用\\が2つあるのでどっちか区別するためにする処理
            if (!shift && showkeypress_mode && input_str == "OemBackslash")
            {
                return "OemBackslash";
            }

            for (int i = 0; i < 48; ++i)
            {
                if (input_str == Cw[i, 0])
                {
                    return !shift ? Cw[i, 1] : Cw[i, 2];
                }
            }
            return null;
        }
        bool ShiftFlag = false;
        // キーの入力があったらFrom1.csのOnKeyDownから呼ぶ
        public void KeypressFunc(string input_str)
        {
            // 文字を分解する時に空文字は取り除く様にオプションを指定
            System.StringSplitOptions option = System.StringSplitOptions.RemoveEmptyEntries;
            // 「,」を区切り文字にstring型の配列linesに文字を分解して代入する
            string[] lines = input_str.Split(new char[] { ',' }, option);
            //シフトを押してるなら
            if (lines.Length == 2)
            {
                ShiftFlag = true;
            }

            // シフトキーしか押してない時は無効
            if (lines[0] == "ShiftKey")
            {
                return;
            }
            // 文字の保存
            string temp_str = GetKeyString(lines[0], ShiftFlag);
            if (temp_str == null) return;
            // もし入力したキー(temp_str)と問題文の文字が同じなら正解
            if(InputTextk[TextPos][StrPosE].ToString() == temp_str)
            {
                InputRichTextBox.Text = InputTextk[TextPos].Substring(StrPosE+1);
                // 1文打ち終わったら次の文を設定する
                if(InputTextk[TextPos].Length <= ++StrPosE)
                {
                    StrPosE = 0;
                    TextPos = (TextPos + 1) % InputTextk.Count;
                    ViewRichTextBox.Text = ViewText[TextPos];
                    InputRichTextBox.Text = InputTextk[TextPos];
                }
            }
        }
    }
}

Form1.csを以下の様に変更

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace TypingGame
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }
        CTyping Typing = null;
        // 1度だけ呼ばれる文章
        private void Form1_Load(object sender, EventArgs e)
        {
            // 問題文のフォント
            int rich_text_font_size = 16;
            richTextBox1.Font = new Font(System.Windows.Forms.Control.DefaultFont.FontFamily, rich_text_font_size, FontStyle.Bold, GraphicsUnit.Pixel);
            richTextBox2.Font = new Font(System.Windows.Forms.Control.DefaultFont.FontFamily, rich_text_font_size, FontStyle.Bold, GraphicsUnit.Pixel);
            Typing = new CTyping(richTextBox1, richTextBox2);
        }
        // キーを押したら
        protected override void OnKeyDown(KeyEventArgs e)
        {
            Typing.KeypressFunc(e.KeyData.ToString());
        }
		    // リッチテキストからフォーカスをボタンに合わせる
        private void richTextBox1_Enter(object sender, EventArgs e)
        {
            button1.Focus();
        }
		    // リッチテキストからフォーカスをボタンに合わせる
        private void richTextBox2_Enter(object sender, EventArgs e)
        {
            button1.Focus();
        }
        // ボタンがクリックされたら
        private void button1_Click_1(object sender, EventArgs e)
        {
            // どのフォーカスでも入力を受け付けるようにする
            KeyPreview = !KeyPreview;
            if(KeyPreview)
            {
                Typing.GameStart();
                button1.Text = "ストップ";
            }
            else
            {
                richTextBox1.Text = string.Empty;
                richTextBox2.Text = string.Empty;
                button1.Text = "スタート";
            }
        }
    }
}

実行結果

今回は最低限タイピングゲームに必要な機能を実装しました。
次回は問題文をファイルから読み込みローマ字入力できるようにしてみます。
そして、こちらから今回のプロジェクトをダウンロードできます。