ジェイのブログ

Unity,UnrealEngine,ゲーム制作,プログラミング,日記も書きます!

個人ゲーム開発者が自作ゲームを流行らせるための2つの方法と1つの秘訣

こんにちは!ジェイです。

個人でゲームを作ってるみなさんは、グラフィックやプログラミングやサウンドや3Dモデルを用意したりと、非常に大変な作業をしてますよね。

そして、その苦労をした分だけ「より多くのプレイヤーに自分の作ったゲーム届けたい」と思いますよね?

 

その際にどう宣伝したら、多くのユーザーにゲームを届けられるか?と悩むことは多いでしょう。

今回は、特にヘビーユーザーをターゲットにしたゲームを流行らせる方法を紹介していきます。

 

個人ゲーム制作者がヘビーユーザーをターゲットにした方がよい理由については、以前説明したので、気になる人はこちらを参考にしてください。

happynetwork2019.hatenablog.com

好みの近いユーザーを自分のゲームに流し込む方法

まず最初は、ターゲットにしているユーザーがどんな人なのか考えてみてください。

例えば、あなたの作っているゲームがマリオカートの様なゲームだったとします。

 

あ、それなら、マリオカートが好きなユーザーに向けてゲームを作った方がよさそう!ということに何となく気づきませんか。そうなんです!実は…

この続きを読むには
購入して全文を読む

Unityで作るローマ字入力とかな入力に両対応した多機能なタイピングゲームの制作講座 4回目

こんにちは!ジェイです。

このUnityタイピングゲーム制作講座も、今回でいよいよ最終回になります。

そして、ついに完全版のJ-TypeEngineのプロジェクト配布します!

※完成版のプロジェクトの配布は有料コンテンツで購入するとダウンロードできます。

 

使い方は配信で解説しましたので動画で見たい方は以下のリンクを参照してください。

Unity用タイピングゲームエンジンJ-TypeEngine配布&説明会! - YouTube

 

スクリプトの違いについて

Unityのバージョンは2022.3.19fでダウンロードはこちらからお願いします。

 

最初に配布したサンプルとJ-TypeEngineの一番の違いOnGUI()での処理をすべてTypingEngine.KeyPress()内に入れたことです。

説明では、MakeInputStr()やMakeSearchStr()などの関数の処理を説明したかったので外に出してましたが、それらはCJTypeEngineクラス内部で処理するようになってます。

>|C#|   
private void OnGUI()
    {
        if (!StartFlag) return;
        Event e = Event.current;
        if (e.type == EventType.KeyDown && e.type != EventType.KeyUp && e.keyCode != KeyCode.None
            && e.keyCode != KeyCode.LeftShift && e.keyCode != KeyCode.RightShift)
        {
            if(TypingEngine.KeyPress(e, LeftShift || RightShift))
            {
                ViewTextMesh.text = "終了";
                InputTextMesh.text = "";
                StartFlag = false;
                StartButton.enabled = true;
            }
        }
    }
||<


J-TypeEngineについて

使用時の注意点

以下の用途は報告なしで使用するのを許可します

・Unityでのタイピングゲーム制作に使う

・こちらの講座のURLを明記は任意です。ただし、自作発言は禁止します。

・J-TypeEngineを使用して発生した如何なる責任も負えません

 

またご意見や質問などもコメントやTwitterのDMなどで受け付けてます。

https://twitter.com/JY20160816

 

 

まず画面を見ながらJ-TypeEngineのタイピングゲームとしての機能を紹介していきます。

ローマ字入力とかな入力の切り替え

「ローマ字」か「かな」にチェックを入れてスタートボタンをクリックすると対応した入力モードでタイピングが始まります。

スクロール基準と最大文字について

左下に入ってる数字を見てみると「かな」と「ローマ字」と「漢字」の3種類についてスクロール基準と最大表示文字数の後に数字が入ってます。

「かなのスクロール基準」と「かな最大表示文字数」

かな入力の時のInputTextに入る文字についての設定です。

例えば、「かなのスクロール基準」に「5」を入れると5番目の文字以降までタイピングすると、その位置を基準にスクロールし始めます。(0から数えるので注意!)

「かな最大表示文字数」に「10」を入れると濁点、半濁点を含む10文字までしか表示されないようになります。

「漢字のスクロール基準」と「漢字最大表示文字数」

ローマ字入力とかな入力両方で表示される文字のスクロールの基準と最大表示文字数です。数え方については、かなの項目と同じです。

「ローマ字のスクロール基準」と「ローマ字最大表示文字数」

ローマ字入力のみで表示されるアルファベットのスクロールの基準と最大表示文字数です。文字数の数え方については、かなの項目と同じです。

ローマ字の表示設定

右下のチェックボックスは、すべてローマ字の表示文字のカスタマイズに使います。

J-TypeEngineでは、ローマ字入力の打ち分け機能が実装されてますが、この部分は表示させるローマ字を好みのものに選択できます。

例えば、SHA,SYU,SYOのチェックを入れるとInputTextに表示されるローマ字がsya,syu,syoに変更されます。

この時に別の打ち方をしたらその通りに打てます。つまりこのチェックボックスは表示のみを好みに設定する項目です

下のチェックボックスのSHA,SHU,SHOにチェックを入れるInputTextのローマ字がshashushoに変更されます。

拗音単体入力無効のチェックボックスにチェックを入れると

「きぃ」「くぃ」「くぅ」「くぇ」「くぉ」「くゃ」「くゅ」「くょ」

などの拗音語を無効にできます。

ワードの追加の仕方

Asset/Resourcesフォルダの中にWordsというファイルがあります。

それを開いてみると以下の様にワードが表示されます。

1行目が漢字ワード(ViewText)に表示される文章で2行目がタイピングする文章(InputText)です。

そして、漢字の文字数とひらがなの文字数を合わせるために以下の様にで囲います。

きちんと漢字の文字数とひらがなの文字数が合えば、漢字ワードも打った時に進むようになります。別に気にならないのであればで囲わなくても、ひらがなやローマ字のワードだけは進むので、無理にやらなくても大丈夫です。

J-TypeEngineを使った作品のご紹介

なんと完成版のプロジェクトを配布した次の日にさっそく絡繰くろあさんが、バトルタイピングゲームを制作してくれました!

たった1日でここまでできてるのは本当にすごいし嬉しいですね(^^♪

もし他にも使ってくれた方がいたら知らせてくれると嬉しいです。

まとめ

全4回にわたってUnityタイピングゲーム制作講座をやってきました。世の中にはかな入力のタイピングゲームがすごく少ないです。

原因は

・ゲーム制作者のほとんどがローマ字入力に慣れているという

・Unityで通常のやり方では日本語キーボードのすべてのキーが取得できない

という理由があります。

なので今回、かな入力である私自身が少しでもかな入力のタイピングゲームが増えたらという想いでこのJ-TypeEngineを制作しました。

タイピングというジャンルは競技性が高くライトユーザーにはとっつきにくい面がある一方で、タイパーと呼ばれる競技タイピングを好む人たちのモチベーションは非常に高いです。

世の中のゲームプレイヤーのモチベーションが下がってる今だからこそ、ゲーム制作者はこういった需要に答えてみてはいかがでしょうか?

 

Unityで作るローマ字入力とかな入力に両対応した多機能なタイピングゲームの制作講座 3回目

こんにちは!ジェイです。

前回まででCTypeEngineクラスのローマ字入力の内部処理を説明しました。

サンプルプロジェクトはこちらから

内容は

・ひらがなからローマ字の文章を生成する(MakeInputStr)

・ひらがなからローマ字の入力候補を生成する(MakeSeachStr)

・入力した文字を元に正誤判定をしてその後に対応した処理をする

だいぶ長い説明になってますが、内容としては以上です。

 

今回はかな入力で同じ内容を説明していきますが、ローマ字入力に比べて判定が1文字ずつで済む分のでシンプルで理解しやすいです。

それでは、さっそく作っていきましょう!

ひらがなから濁点と半濁点を分解して入力文章を作る

ローマ字入力では、辞書から検索して対応するアルファベットを入れて変換する作業があって大変でしたが、かな入力ではひらがなの濁点と半濁点を分解するだけなので簡単です。

まずは、検索するための辞書になる2次元配列Stについてみてみます。

>|C#| 
private string[,] DecStr = new string[,]
{
        {"が","か","゛"},
        {"ぎ","き","゛"},
        {"ぐ","く","゛"},
        {"げ","け","゛"},
        {"ご","こ","゛"},
        {"ざ","さ","゛"},
        {"じ","し","゛"},
        {"ず","す","゛"},
        {"ぜ","せ","゛"},
        {"ぞ","そ","゛"},
        {"だ","た","゛"},
        {"ぢ","ち","゛"},
        {"づ","つ","゛"},
        {"で","て","゛"},
        {"ど","と","゛"},
        {"ば","は","゛"},
        {"び","ひ","゛"},
        {"ぶ","ふ","゛"},
        {"べ","へ","゛"},
        {"ぼ","ほ","゛"},
        {"ぱ","は","゜"},
        {"ぴ","ひ","゜"},
        {"ぷ","ふ","゜"},
        {"ぺ","へ","゜"},
        {"ぽ","ほ","゜"},
        {"ヴ","ウ","゛"},
        {"ガ","カ","゛"},
        {"ギ","キ","゛"},
        {"グ","ク","゛"},
        {"ゲ","ケ","゛"},
        {"ゴ","コ","゛"},
        {"ザ","サ","゛"},
        {"ジ","シ","゛"},
        {"ズ","ス","゛"},
        {"ぜ","セ","゛"},
        {"ゾ","ソ","゛"},
        {"ダ","タ","゛"},
        {"ヂ","チ","゛"},
        {"ヅ","ツ","゛"},
        {"デ","テ","゛"},
        {"ド","ト","゛"},
        {"バ","ハ","゛"},
        {"ビ","ヒ","゛"},
        {"ブ","フ","゛"},
        {"ベ","ヘ","゛"},
        {"ボ","ホ","゛"},
        {"パ","ハ","゜"},
        {"ピ","ヒ","゜"},
        {"プ","フ","゜"},
        {"ペ","ヘ","゜"},
        {"ポ","ホ","゜"},
};
||<

string型の二次元配列DecStrの[0,0]には濁点または半濁点のひらがなの文字が入っていて、ループ分を用いて検索しこれと同じ文字を発見した時に[0,1]と[0,2]にある文字を分解してstrに保存します。

が→か+゛

もし検索しても辞書になかったらそのままひらがなを1文字代入するという処理を繰り返して、ひらがなの文章を1行すべて分解します。

>|C#|
public class CKanaTypeEngine : CTypeEngine
{    
    const int ST_COL = 227;
    const int ST_ROW = 3;
    string[,] St = new string[ST_COL, ST_ROW];
public override string MakeInputText(string input_textk)
    {
        string str = string.Empty;
        for (int i = 0; i < input_textk.Length; ++i)
        {
            bool update = false;
            for (int j = 0; j < DecStr.GetLength(0); ++j)
            {
                if (DecStr[j, 0] == input_textk[i].ToString())
                {
                    update = true;
                    str += DecStr[j, 1] + DecStr[j, 2];
                    break;
                }
            }
            if (!update)
            {
                str += input_textk[i];
            }
        }
        NowInputText = str;
        return str;
  }
}
||<

MakeInputStrの処理は以上ですべてです。

ローマ字入力に比べると複雑な処理がいらないしコードの量もとても少ないですよね。

ひらがなから入力判定用の文字を作る(MakeSearchStr)

こちらもローマ字入力の時と同じように、入力時に判定するための検索文字を保存しておく処理を行います。

単純にSt[i,0]に入っている1文字とひらがなの文章の1文字を比較してから、もし同じ文字があったら、シフト押してない時の1文字のSt[i,1]とシフト押している時の1文字のSt[i,2]を検索文字配列SearchStrsにそれぞれ保存しておきます。

St[i,1]だけに文字が入っている場合はシフトなしの時のみ正解判定になって、St[i,2]にも文字が入っている場合は、シフトを押していてもどちらでも正解判定になります。

>|C#|
    public override List<string> MakeSearchStr(string input_textk, int str_posk)
    {
        SearchStrs[0] = string.Empty;
        SearchStrs[1] = string.Empty;
        string str_buf = MakeInputText(input_textk).Substring(str_posk, 1);
        for (int i = 0; i < ST_COL; ++i)
        {
            if (str_buf == St[i, 0])
            {
                SearchStrs[0] = St[i, 1];
                SearchStrs[1] = St[i, 2];
                break;
            }
        }
        return SearchStrs.ToList();
  }
||<

これだけで入力判定用検索文字を保存できました!

1つだけ注意する点は先ほど説明したMakeInputStr()を読んで濁点と半濁点を分解する処理をしてから入力判定用検索文字の保存を行っていることです。

キー入力から正誤判定をしてそれぞれに対応した処理をする

これもローマ字入力に比べて非常に単純でGetKeyString()にkeyとShiftを渡して2つ分の情報を1つにしてtemp_strに格納します。

それと先ほどMakeSearchStrで保存しておいた検索用の文字が格納されてるSerachStrs配列の要素のどちらかと同じであったら、正解でTextPosが引数で渡されたinput_textkの要素数と同じになったら、タイピング文章が打ち終わったことになるので、終了処理を行います。

 

MakeInputStr()は1つの文章につき1回呼んで、MakeSearchStr()は1文字正解して打ち終わるごとに処理を実行します。ミスをした時にはフラグを立てるだけなので難しいことはないです!

>|C#|
protected override bool InputJudge(string[] input_textk, string key, bool shift)
{
    string temp_str = GetKeyString(key, shift);
    if (temp_str == null) return false;
    // 一致判定
    if (SearchStrs[0] == temp_str || SearchStrs[1] == temp_str)
    {
        TypeMissFlag = false;
        // 一文終了
        if (++StrPosK >= NowInputText.Length)
        {
            InputPos = 0;
            StrPosK = 0; // 入力文(かな)ポジション
            // 全文終了
            if (input_textk.Length <= ++TextPos)
            {
                TextPos = 0;
                return true;
            }
            // 次のかな文章を作成してInputTextに代入
            NowInputText = MakeInputText(input_textk[TextPos]);
            if (NowInputText == null)
            {
                Debug.Log("NowInputText is null");
                return false;
            }
        }
        MakeSearchStr(input_textk[TextPos], StrPosK);
        InputPos = StrPosK;
    }
    else // ミス時
    {
        TypeMissFlag = true;
    }
    return false;
}
||<

まとめ

・ひらがなから濁点と半濁点を分解した文章を生成する(MakeInputStr)

・ひらがなからシフトなしの時の正解文字とシフトありの時の正解文字を入力候補として保存する(MakeSeachStr)

・入力した文字を元に正誤判定をしてその後に対応した処理をする

 

ローマ字入力とまったく同じ関数名で同じ役割の処理なのに関わらず、中身の処理を見てみるとまるで別物なのがわかります。

 

それを、すべての基底クラスであるCTypeEngineクラスのメソッドをオーバーライドして中身を全部入れ替えて、ローマ字入力とかな入力に対応できるのは素晴らしいですよね!

これでローマ字入力だけでなく、かな入力と両方に対応したタイピングゲームを作る知識を最低限ではありますが説明しました。

 

ただ今回のサンプルは説明のために無理にクラスの外に出してしまったために、よくない点がいくつかあるのに気づいた人もいるでしょう。

次回でそれらを修正した上で、完全版のプロジェクトを配布するとともにすべての機能の説明をして最後の講座にする予定です。それではまた次回会いましょう~!

Unityで作るローマ字入力とかな入力に両対応した多機能なタイピングゲームの制作講座 2回目

こんにちは!ジェイです。

前回に引き続き、Unityタイピングゲーム制作講座やっていきます!

happynetwork2019.hatenablog.com

前回まで説明したこと

前回、配布したunityプロジェクト内に以下の3つスクリプトがあります。ダウンロードはこちら

CTypeEngine

CRomaTypeEngine(CTypeEngineを継承)

CKanaTypeEngine(CTypeEngineを継承)

CRomaTypeEngineとCKanaTypeEngineのどちらかをインスタンス化するかによってローマ字入力とかな入力の処理を切り替えられるのを実践しました。

CTypeEngine TypeEngine = new CRomaTypeEngine(); // ローマ字入力

CTypeEngine TypeEngine = new CKanaTypeEngine(); // かな入力

 

C#の多様性を利用して、インスタンス化した以降に呼ばれるオーバーライドした関数は、そのクラス内の関数が呼ばれることになります。

new CRomaTypeEngine();とした場合は、CRomaTypeEngineクラス内に定義したMakeInputStr()やMakeSearchStrが呼ばれて、new CKanaTypeEngine();とした場合は、CKanaTypeEngineクラス内のMakeInputStr()やMakeSearchStrが呼ばれます。

 

タイピングゲームを作るにあたって重要な処理は

1.ひらがなからタイピング文章を生成する(MakeInputStr)

2.ひらがなから入力判定するのに必要な文字を作る(MakeSearthStr)

3.キー入力から正誤判定をして正解、間違いそれぞれの処理を行う

4.テキストメッシュに代入して文字を表示させる

 

前回は、以上の大まかな流れを説明しました。今回は1~3でCTypeEngine内の処理を具体的に説明していきます。

ローマ字入力の場合

まず最初にCRomaTypeEngineをインスタンス化した場合の処理について考えてみましょう。

ひらがなからローマ字を生成する処理(MakeInputStr)

MakeInputStr("であいとわかれのきせつ");と入力すると「deaitowakarenokisetu」と出力されます。

このようにひらがなを見てローマ字の文章を出力するのがMakeInputStr関数の役割です。

では、具体的に処理を見てどうやってローマ字にしているのか見てみます。

結論から言うとひらがな4文字分見て辞書登録してある文字と比べて同じなら、4文字分のひらがなをローマ字に変換して次の文字へ

それでも、もし見つからなかったら3文字分見て見つかったら3文字分のひらがなをローマ字に変換して次の文字へ

それでも、もし見つからなかったら2文字分見て見つかったら3文字分のひらがなをローマ字に変換して次の文字へ

それでも、もし見つからなかったら1文字分見て見つかったら3文字分のひらがなをローマ字に変換して次の文字へ

最期まで見つからなかったらエラーを出力する

という処理を繰り返していけば、すべてのローマ字に対応して変換できます。

 

更に「ん」や「っ」は特殊な打ち分けができるために正確に処理の流れを説明すると

「ん」が先頭の4文字を調べて見つかったらローマ字に変換してewordに追加、見つからなかったら「ん」が先頭の3文字を調べて見つかったらローマ字に変換してewordに追加というのを最後の1文字「ん」になるまで繰り返します。

 

「っ」も同じ要領で先頭の4文字を調べて見つかったらローマ字に変換してewordに追加、見つからなかったら「っ」が先頭の3文字を調べて見つかったらローマ字に変換してewordに追加というのを見つからなかった場合のみ1文字「っ」になるまで繰り返します。

 

ここまでの処理で特殊なローマ字の生成は終わるので、最後は通常の文字と辞書を比較して、同じように見つからなかった場合のみ1文字の「っ」と比較してローマ字をewordに追加します。

>|C#|
public class CRomaTypeEngine : CTypeEngine
{
public override string MakeInputText(string input_textk)
{
  string eword = string.Empty;
  string subtext = input_textk;
  string strbuf;
  bool exflg;
  int err = 0;
  int textlength;
  while (!String.IsNullOrEmpty(subtext))
  {
      if (++err > 700)
      {
          Debug.Log("SubTextが読み取れません");
          return null;
      }
      textlength = subtext.Length;
      exflg = false;
      strbuf = subtext.Substring(0, 1);
      if (strbuf == "ん" || strbuf == "ン") // 一文字目が「ん」
      {
          //「ん」「っ」付き4文字
          if (!exflg && textlength >= 4)
          {
              for (int k = 0; k < STNTEX_COL; k++)
              {
                  if (subtext.Substring(0, 4) == Stntex[k, 0])
                  {
                      eword += Stntex[k, 1];
                      // subtextを4文字目から最後まで切り取る(最初の4文字を切り捨てる)
                      subtext = subtext.Substring(4, textlength - 4);
                      exflg = true;
                      break;
                  }
              }
          }
          //「ん」「っ」付き3文字
          if (!exflg && textlength >= 3)
          {
              for (int k = 0; k < STNT_COL; k++)
              {
                  if (subtext.Substring(0, 3) == Stnt[k, 0])
                  {
                      eword += Stnt[k, 1];
                      // subtextを3文字目から最後まで切り取る(最初の3文字を切り捨てる)
                      subtext = subtext.Substring(3, textlength - 3);
                      exflg = true;
                      break;
                  }
              }
          }
          //「ん」付き3文字
          if (!exflg && textlength >= 3)
          {
              for (int k = 0; k < STNEX_COL; k++)
              {
                  if (subtext.Substring(0, 3) == Stnex[k, 0])
                  {
                      eword += Stnex[k, 1];
                      // subtextを3文字目から最後まで切り取る(最初の3文字を切り捨てる)
                      subtext = subtext.Substring(3, textlength - 3);
                      exflg = true;
                      break;
                  }
              }
          }
          //「ん」付き2文字
          if (!exflg && textlength >= 2)
          {
              for (int k = 0; k < STN_COL; k++)
              {
                  if (subtext.Substring(0, 2) == Stn[k, 0])
                  {
                      eword += Stn[k, 1];
                      // subtextを2文字目から最後まで切り取る(最初の2文字を切り捨てる)
                      subtext = subtext.Substring(2, textlength - 2);
                      exflg = true;
                      break;
                  }
              }
          }
          if (!exflg)
          {
              eword += "nn";
              // subtextを1文字目から最後まで切り取る(最初の1文字を切り捨てる)
              subtext = subtext.Substring(1, textlength - 1);
          }
      }
      else if (strbuf == "っ" || strbuf == "ッ") // 一文字目が「っ」
      {
          //「っ」付き3文字
          if (!exflg && textlength >= 3)
          {
              for (int k = 0; k < STTEX_COL; k++)
              {
                  if (subtext.Substring(0, 3) == Sttex[k, 0])
                  {
                      eword += Sttex[k, 1];
                      // subtextを3文字目から最後まで切り取る(最初の3文字を切り捨てる)
                      subtext = subtext.Substring(3, textlength - 3);
                      exflg = true;
                      break;
                  }
              }
          }
          //「っ」付き2文字
          if (!exflg && textlength >= 2)
          {
              for (int k = 0; k < STT_COL; k++)
              {
                  if (subtext.Substring(0, 2) == Stt[k, 0])
                  {
                      eword += Stt[k, 1];
                      // subtextを2文字目から最後まで切り取る(最初の2文字を切り捨てる)
                      subtext = subtext.Substring(2, textlength - 2);
                      exflg = true;
                      break;
                  }
              }
          }
          if (!exflg)
          {
              eword += "ltu";
              // subtextを1文字目から最後まで切り取る(最初の1文字を切り捨てる)
              subtext = subtext.Substring(1, textlength - 1);
          }
      }
      else // その他
      {
          //2文字
          if (!exflg && textlength >= 2)
          {
              for (int k = 0; k < STEX_COL; k++)
              {
                  // subtextを先頭から2文字切り取る
                  if (subtext.Substring(0, 2) == Stex[k, 0])
                  {
                      eword += Stex[k, 1];
                      // subtextを2文字目から最後まで切り取る(最初の2文字を切り捨てる)
                      subtext = subtext.Substring(2, textlength - 2);
                      exflg = true;
                      break;
                  }
              }
          }
          //1文字
          if (!exflg)
          {
              for (int k = 0; k < ST_COL; k++)
              {
                  if (subtext.Substring(0, 1) == St[k, 0])
                  {
                      if (St[k, 0] == " " || St[k, 0] == " ")
                      {
                          eword += " ";
                      }
                      else
                      {
                          eword += St[k, 1];
                      }
                      // subtextを1文字目から最後まで切り取る(最初の1文字を切り捨てる)
                      subtext = subtext.Substring(1, textlength - 1);
                      exflg = true;
                      break;
                  }
              }
          }
      }
  }      
  NowInputText = eword;
  return eword;
}
}
||<

どんなに複雑なローマ字入力でもこのように最初に用意しておいた辞書と比較していけば、ひらがなからローマ字を生成する処理が実行できます。

辞書を登録する処理は長くなるので割愛しますが、CRomaTypeEngineのコンストラクタで単に配列に文字を入れているだけなので、見ていただければわかると思います。

ひらがなから入力判定するのに必要な文字を作る(MakeSearthStr)

先ほどのひらがなからローマ字を生成するMakeInputStr()によく似ていますが、MakeInputStr()はひらがなから1文章を最後までローマ字を生成するのに対して、MakeSearchStr()はひらがなからタイピングした時に入力が正解か間違いか判定するためのローマ字候補を生成する処理を行います。

        foreach (var str in TypeEngine.MakeSearchStr("であいとわかれのきせつ", 10))
        {
            Debug.Log(str);
        }

出力 tu tsu

第1引数にひらがなの文章を入れて第2引数に何文字目のひらがなを変換するかというのを入れます。

「つ」なら入力候補の「tu」と「tsu」が入って、「か」なら入力候補の「ka」と「ca」がSearchStrsに格納されます。

MakeInputStr()とひらがなの4文字分比較して見つからなかったら、3文字分比較するという処理はほとんど似ています。

>|C#|
public class CRomaTypeEngine : CTypeEngine
{
public override List<string> MakeSearchStr(string input_textk, int str_posk)
{
  int i, k;
  SearchStrs.Clear();
  string subtext;
  bool exflg = false;
  string strbuf = string.Empty;
  List<string> return_str = new List<string>();
  return_str.Clear();
  strbuf = input_textk.Substring(str_posk, 1);
  int restword = input_textk.Length - str_posk;
  if (strbuf == "ん" || strbuf == "ン") // 一文字目が「ん」
  {
      //「ん」「っ」付き4文字 12候補  1-11:4文字 12-21:3文字 22-29:2文字 30-33:1文字
      if (!exflg && restword >= 4)
      {
          subtext = input_textk.Substring(str_posk, 4);
          for (i = 0; i < STNTEX_COL; i++)
          {
              if (subtext == Stntex[i, 0])
              {
                  for (k = 1; k < STNTEX_ROW; k++)
                  {
                      if (string.IsNullOrEmpty(Stntex[i, k])) continue;
                      SearchStrs.Add(new TagSearchStrings(Stntex[i, k], 4));
                      return_str.Add(Stntex[i, k]);
                  }
                  exflg = true;
                  break;
              }
          }
      }
      //「ん」「っ」付き3文字 10候補
      if (!exflg && restword >= 3)
      {
          subtext = input_textk.Substring(str_posk, 3);
          for (i = 0; i < STNT_COL; i++)
          {
              if (subtext == Stnt[i, 0])
              {
                  for (k = 1; k < STNT_ROW; k++)
                  {
                      if (string.IsNullOrEmpty(Stnt[i, k])) continue;
                      SearchStrs.Add(new TagSearchStrings(Stnt[i, k], 3));
                      return_str.Add(Stnt[i, k]);
                  }
                  exflg = true;
                  break;
              }
          }
      }
      //「ん」付き3文字 6候補
      if (!exflg && restword >= 3)
      {
          subtext = input_textk.Substring(str_posk, 3);
          for (i = 0; i < STNEX_COL; i++)
          {
              if (subtext == Stnex[i, 0])
              {
                  for (k = 1; k < STNEX_ROW; k++)
                  {
                      if (string.IsNullOrEmpty(Stntex[i, k])) continue;
                      SearchStrs.Add(new TagSearchStrings(Stnex[i, k], 3));
                      return_str.Add(Stnex[i, k]);
                  }
                  exflg = true;
                  break;
              }
          }
      }
      //「ん」付き2文字 8候補
      if (!exflg && restword >= 2)
      {
          subtext = input_textk.Substring(str_posk, 2);
          for (i = 0; i < STN_COL; i++)
          {
              if (subtext == Stn[i, 0])
              {
                  for (k = 1; k < STN_ROW; k++)
                  {
                      if (string.IsNullOrEmpty(Stn[i, k])) continue;
                      SearchStrs.Add(new TagSearchStrings(Stn[i, k], 2));
                      return_str.Add(Stn[i, k]);
                  }
                  exflg = true;
                  break;
              }
          }
      }
      if (!exflg)
      {
          SearchStrs.Add(new TagSearchStrings("nn", 1));
          SearchStrs.Add(new TagSearchStrings("xn", 1));
          return_str.Add("nn");
          return_str.Add("xn");
      }
  }
  else if (strbuf == "っ" || strbuf == "ッ") // 一文字目が「っ」
  {
      //「っ」付き3文字 6候補 0-11:4文字 12-21:3文字 22-29:2文字 30-33:1文字
      if (!exflg && restword >= 3)
      {
          subtext = input_textk.Substring(str_posk, 3);
          for (i = 0; i < STTEX_COL; i++)
          {
              if (subtext == Sttex[i, 0])
              {
                  for (k = 1; k < STTEX_ROW; k++)
                  {
                      if (string.IsNullOrEmpty(Sttex[i, k])) continue;
                      SearchStrs.Add(new TagSearchStrings(Sttex[i, k], 3));
                      return_str.Add(Sttex[i, k]);
                  }
                  exflg = true;
                  break;
              }
          }
      }
        //「っ」付き2文字 5候補
        if (!exflg && restword >= 2)
        {
            subtext = input_textk.Substring(str_posk, 2);
            for (i = 0; i < STT_COL; i++)
            {
                if (subtext == Stt[i, 0])
                {
                    for (k = 1; k < STT_ROW; k++)
                    {
                        if (string.IsNullOrEmpty(Stt[i, k])) continue;
                        SearchStrs.Add(new TagSearchStrings(Stt[i, k], 2));
                        return_str.Add(Stt[i, k]);
                    }
                    exflg = true;
                    break;
                }
            }
        }
        if (!exflg)
        {
            SearchStrs.Add(new TagSearchStrings("ltu", 1));
            SearchStrs.Add(new TagSearchStrings("xtu", 1));
            SearchStrs.Add(new TagSearchStrings("ltsu", 1));
            return_str.Add("ltu");
            return_str.Add("xtu");
            return_str.Add("ltsu");
        }
    }
    else  // その他
    {
        //2文字 5候補  0-11:4文字 12-21:3文字 22-29:2文字 30-33:1文字
        if (!exflg && restword >= 2)
        {
            subtext = input_textk.Substring(str_posk, 2);
            for (i = 0; i < STEX_COL; i++)
            {
                if (subtext == Stex[i, 0])
                {
                    for (k = 1; k < STEX_ROW; k++)
                    {
                        if (string.IsNullOrEmpty(Stex[i, k])) continue;
                        SearchStrs.Add(new TagSearchStrings(Stex[i, k], 2));
                        return_str.Add(Stex[i, k]);
                    }
                    exflg = true;
                    break;
                }
            }
        }
        if (!exflg)
        {
            subtext = input_textk.Substring(str_posk, 1);
            for (i = 0; i < ST_COL; i++)
            {
                if (subtext == St[i, 0])
                {
                    for (k = 1; k < ST_ROW; k++)
                    {
                        if (string.IsNullOrEmpty(St[i, k])) continue;
                        SearchStrs.Add(new TagSearchStrings(St[i, k], 1));
                        return_str.Add(St[i, k]);
                    }
                    break;
                }
            }
        }
    }
    // 検索配列のローマ表示テキストにおける先頭文字位置(入力時の文字が表示している例と違う場合の修正用)
    foreach (var search_str in SearchStrs)
    {
        if (!String.IsNullOrEmpty(search_str.SearchStr))
        {
            // 現在1番手候補の文字長さ保存
            EnglishLen = search_str.SearchStr.Length;
            break;
        }
    }
    return return_str;
}
||<

MakeInputStr()との違いは、1文章を全部終わりまで処理するのではなくて、1ワード分の入力候補を生成することです。

「あ」→「a」

「しゃ」→「sya」「sha」

「ちゃ」→「tya」「cha」「cya」

以上のような感じで辞書から、ローマ字で入力する候補すべてをSearchStrsに格納しています。

このタイピングゲームで一番大事な「ひらがなからローマ字を生成する(MakeInputStr)」と「ひらがなからローマ字入力候補を保存しておく(MakeSearchStr)」の説明が終わりました。

最期に入力した時の判定について考えてみましょう!

入力判定処理

CGameManagerのOnGUIでは以下の様にキーを取得してCTypeEngineクラスのKeyPress関数にキーの情報を渡しています。

するとタイピングが終了したかがbool型で返ってくるので、それに元づいて文字の色を変更して表示してるのは前回説明しました。

>|C#|
public class CGameManager : MonoBehaviour
{
private void OnGUI()
{
Event e = Event.current;
  if (e.type == EventType.KeyDown && e.type != EventType.KeyUp && e.keyCode != KeyCode.None
        && e.keyCode != KeyCode.LeftShift && e.keyCode != KeyCode.RightShift)
  {
      if (TypeEngine.KeyPress(InputText, e, LeftShift || RightShift))
       {
          ViewTextMesh.text = "終了";
          InputTextMesh.text = "";
       }
       else
       {
           // 漢字文字を表示させる
           ViewTextMesh.text = ViewText[TypeEngine.TextPos];
           // すでに打ち終わった文字
           string str1 = "<color=#808080>" + TypeEngine.NowInputText.Substring(0, TypeEngine.InputPos) + "</color>";
           // これから打つ1文字
           string str2 = string.Empty;
           if (TypeEngine.TypeMissFlag)
           {
               str2 = "<color=#FF0000>" + TypeEngine.NowInputText.Substring(TypeEngine.InputPos, 1) + "</color>";
           }
           else
           {
               str2 = "<color=#0000FF>" + TypeEngine.NowInputText.Substring(TypeEngine.InputPos, 1) + "</color>";
           }
           // これから打つ1文字より後ろの最期の文字まで
           string str3 = "<color=#FFFFFF>" + TypeEngine.NowInputText.Substring(TypeEngine.InputPos + 1) + "</color>";
           // 入力文字に反映させる
           InputTextMesh.text = str1 + str2 + str3;
}
}
}
}
||<

今回はKeyPress関数について説明していきます。

この部分が今回のサンプルを作るにあたって一番苦労したところで、Unityでタイピングゲームを作る際にハマるポイントだと思います。

具体的にはInputManagerやInputSystemを使用すると「ー」や「ろ」のキーの判定が上手くできないです。

なので、以下のようにOnGUIでキーイベントを取得して、BackSlashかどうか判定して、キーコードが226なら「ー」でそうでないなら「ろ」でBackSlashでなければ、InputJudgeに渡しています。

>|C#|
public class CTypeEngine
{
private readonly KeyCode[] keyCodes = Enum.GetValues(typeof(KeyCode)).Cast<KeyCode>().ToArray();
public bool KeyPress(string[] input_textk, Event e, bool shift)
{
  bool type_end = false;
  foreach (KeyCode key_code in keyCodes)
  {
      if (e.keyCode == key_code)
      {
          if ((e.keyCode == KeyCode.Backslash && e.functionKey) || e.keyCode.ToString() == "226")
          {
              // ろ
              type_end = InputJudge(input_textk, "ろ", shift);
          }
          else if (e.keyCode == KeyCode.Backslash)
          {
              // ー
              type_end = InputJudge(input_textk,"ー", shift);
          }
          else
          {
              type_end = InputJudge(input_textk, key_code.ToString(), shift);
          }
          break;
      }
  }
  return type_end;
}
}
||<

このKeyPress()の処理はローマ字入力、かな入力ともに共通してる部分なので基底クラスのCTypeEingeに定義してます。

その後の処理は、ローマ字入力とかな入力では、まったく別の処理をするので、InputJudgeをオーバーライドして、それを実現してます。

>|C#|
public class CRomaTypeEngine : CTypeEngine
{
protected override bool InputJudge(string[] input_textk, string key, bool shift)
    {
        string temp_str = GetKeyString(key, shift);
        if (temp_str == null) return false;
        StrBuf += temp_str;
        bool agreeflg = false;
        foreach (var search_str in SearchStrs)
        {
            // 完全一致した場合(一文字終了)
            if (search_str.SearchStr == StrBuf)
            {
                if (1 < search_str.SearchStr.Length)
                {
                    // 表示文字と違う場合修正
                    StrChangedFlag = StrChange(ref NowInputText,
                        ref EnglishLen, StrBuf, SearchStrs, EnglishPos);
                }
                StrBuf = string.Empty;
                // 4文字適合
                if (search_str.SearchNum == 4)
                {
                    StrPosK += 4;
                }
                // 3文字適合
                else if (search_str.SearchNum == 3)
                {
                    StrPosK += 3;
                }
                // 2文字適合
                else if (search_str.SearchNum == 2)
                {
                    StrPosK += 2;
                }
                else // 1文字適合
                {
                    ++StrPosK;
                }
                StrPosK -= KanaPos; // かな表示の途中経過分を差し引く
                KanaPos = 0;
                EnglishPos = ++StrPosE;
                TypeMissFlag = false;
                // 一文終了時
                if (NowInputText.Length <= StrPosE)
                {
                    InputPos = 0;
                    StrPosK = 0; // 入力文(かな)ポジション
                    StrPosE = 0; // ローマ字文ポジション
                    EnglishPos = 0;
                    // 全文終了
                    if (input_textk.Length <= ++TextPos)
                    {
                        TextPos = 0;
                        return true; // タイプ終了
                    }
                    // 次のローマ字文章を作成してInputTextEに代入
                    NowInputText = MakeInputText(input_textk[TextPos]);
                    if (NowInputText == null)
                    {
                        Debug.Log("InputTextE is null");
                        return false;
                    }
                }
                MakeSearchStr(input_textk[TextPos], StrPosK);
                //DrawTextMeshPro(InputTextMesh, NowInputText, StrPosE, ScrollBaseE, ScrollDispMaxE, TypeMissFlag);
                InputPos = StrPosE;
                return false; // タイプ継続
            }
            else if (!string.IsNullOrEmpty(search_str.SearchStr)) // SearchStrが空かどうか調べる
            {
                if (StrBuf.Length <= search_str.SearchStr.Length) // 文字を切り取りすぎないための処理
                {
                    if (search_str.SearchStr.Substring(0, StrBuf.Length) == StrBuf)
                    {
                        agreeflg = true;
                        break;
                    }
                }
            }
        }
        // 一致してない場合のみここを通る(完全一致の場合は上でreturnされるのでここは通らない
        // 合致していなければバッファから消す
        if (!agreeflg) // タイプ文字が不正解の時の処理
        {
            TypeMissFlag = true;
            StrBuf = StrBuf.Substring(0, StrBuf.Length - 1); //間違えた一文字をバッファから消す             
        }
        else // タイプ文字が正解の時の処理
        {
            TypeMissFlag = false;
            string str_temp = input_textk[TextPos].Substring(StrPosK, 1);
            if (StrBuf == "ltu" || StrBuf == "xtu")
            {
                if (str_temp == "っ" || str_temp == "ッ") { StrPosK++; KanaPos = 1; }
            }
            else if (StrBuf == "nn")
            {
                if (str_temp == "ん" || str_temp == "ン") { StrPosK++; KanaPos = 1; }
            }
            StrChangedFlag = StrChange(
                ref NowInputText, ref EnglishLen, StrBuf, SearchStrs, EnglishPos
            );
            InputPos = ++StrPosE;
        }     
        return false;
    }
}
||<

InputJudge()の最初でGetKeyString(key, shift)としてますが、これはシフトを押しているかどうかの情報を1文字だけに詰めるためです。これにより、キーコードとシフトの情報が1つの文字にまとまって使いやすくなります。

 

その後は、一旦StrBufに格納して、その文字がMakeSearchStr()で保存した検索文字と比較します。

ローマ字入力は、ひらがな1文字で2文字分の判定をしなければならないので、部分的に正解した場合と完全に正解した場合の2パターンの正解判定をします。

「し」の場合は「si」と「shi」のどちらでも入力できるので、例えば、StrBufの1文字目が「s」なら部分的に正解と判定します。

その次の入力した文字が「i」の場合は、SearchStrに「si」と「shi」が入っているので、「si」と完全一致して、正解になります。

もしこれが「h」の場合は、部分正解になって、StrBuf内の文字は「sh」になります。続いて「i」が入力された場合には、完全正解になってStrBufは空文字で初期化されます。

不正解だった場合には、その入力文字だけStrBufから削られて不正解の処理を実行します。

また、ローマ字入力では、複数の打ち方があるので、完全一致で正解した時も表示されてる文字と違っていても正解の場合もあります。その時には、表示されてる文字を入力した文字に変更してから、ひらがなに対応した文字数(1~4文字)進めます。

例えば、完全一致した文字が

「あ」ならひらがなに対応した文字を1文字進める

「しゃ」ならひらがなに対応した文字を2文字進める

「んしゃ」ならひらがなに対応した文字を3文字進める

「んっしゃ」ならひらがなに対応した文字を4文字進める

といった感じです。

 

次に1つの文章を全部打ち終わったか判定して、終わっていたらTextPosを1加算して次の文章に進めてから、MakeInputText()で次の文章のローマ字を作成して関数を抜けます。

打ち終わってなければ、MakeSearchStr()で次の検索候補をSearchStrsに保存して、関数を抜けます。

 

基本的には文章が完全に打ち終わるまでこれの繰り返しです。

抑えておくポイントは、正解時には完全一致正解と不完全一致正解があって、そのすべてに当てはまらなければ、不正解の処理を行うというのが一番大事です。

 

かなり長くなってしまいましたが、CTypeEngine内部の処理とローマ字入力での具体的な例を上げて説明しました。

細かい部分を説明するとまだまだありますが、自分独自のタイピングゲームを作るとしたら、大まかな流れだけわかっていれば作れるので十分だと思います。

ソースコードの気になるところに、F9ブレークポイント仕掛けてF5で実行して、F10のステップオーバーやF11のステップインなどしてみたり、Debug.Logで出力して結果を見てみると理解が深まるので、ぜひやってみてください!

デバッグのやり方【Unity初心者入門講座】【ゲームの作り方】#31 - YouTube

 

次回は、かな入力での説明をしてから、最後には完成版のプロジェクトを配布して、それを元にオリジナルのタイピングゲームを作るために必要な知識を解説していきます。

それではまた次の講座でお会いしましょう~!

 

Unityで作るローマ字入力とかな入力に両対応した多機能なタイピングゲームの制作講座 1回目

こんにちは!ジェイです。

タイピングゲームを作ろうと思った時に、「Unity タイピングゲーム 作り方」で検索するとたくさん情報が出てきます。

それを参考に作ってみるとある程度のタイピングゲームは作れるし、ローマ字入力のみのタイピングゲームは見かけることもあります。

しかし、ローマ字とかな入力両方に対応してなおかつ、ローマ字の柔軟な入力対応している情報はたくさんあるのに今のところ、まったくありませんでした。

そこでUnity初心者でも、シンプルかつどんな状況にも対応できるような多機能なタイピングゲームの作り方をこれから説明します。

対象者

・Unityの基本操作ができてC#の基本的な文法(クラス、変数、関数)などは理解できている

タイピングソフトを作るのに必要な処理

・問題文章の読み込み打つ文字に変換してデータを格納する

・データをテキストを表示

・どのキーを入力したか取得して正誤を判定する

・正解、ミスした時にそれぞれの処理を行う

・タイピングが終了した時に結果を表示する

大きく分けると以上の処理になります。

データの読み込みの処理では、ローマ字入力でもかな入力でも、同じ処理で済みますが、それ以降の入力判定や終了などは、ローマ字入力とかな入力で異なる処理を実行しなければなりません。

それに加えてローマ字入力は、打ち方が複数あるので、そのすべてに対応するのはなかなか大変です。

そこで、私が作ったタイピングゲーム用のスクリプトを用いて、必要最低限の入力判定から表示までの流れを行ってみます。

準備

最初からプロジェクトを作る場合は以下通りに作業ください。その際にはこちらからサンプルプロジェクトをダウンロードしてScriptフォルダからCTypeEngineとCTypeRomaEngine.csとCTypeKanaEngine.csの3つのスクリプトをコピーしてください。

CGameManager.csに関しては、自分で作って空のオブジェクトにアタッチして、サンプルを見ながら色々試してみましょう!

しかし、その前にTextMeshProを使えるようにしておきたいので、以下の手順を済ませておきましょう。

 

1.UnityHubを開いて3Dを選び好きな名前でプロジェクトを作成する

2.ヒエラルキーで右クリック→UI→テキスト-TextMeshProでTextMeshProを2つ配置する

最初に以下のウィンドウが出るのでImport TMP Essentialsをクリックする

3.TextMeshProで日本語を使えるようにするために以下の手順を実行します

www.midnightunity.net

フォントをダウロードしてstatic/NotoSansJP-Medium.ttfをUnityでFontフォルダを作ってその中にドラッグ&ドロップします。

上記の注意点としては、japanese_full.txtをダウンロードして貼り付けてしまうとテキストエディタの設定されている文字コードに変換されてエラーが出てしまうことがあります。

DownloadZIPでダウンロードせずにRawをクリックしてコピペしても動くので、できない場合はこちらの方法を試してみてください。

ここまでで日本語が表示できるようになったので、こちらからサンプルプロジェクトをダウンロードしてプロジェクトを開くと以下の様になります。

実行してみるとすぐにローマ字入力でタイピングができるようになっています。

gyazo.com

このプロジェクトは最低限ではありますが、タイピングゲームに必要な処理がすべて実装してます。

具体的に書き出すと

・ひらがなからローマ字を生成する

・ひらがなから入力判定するのに必要な文字を作る

・キー入力から正誤判定をして正解なら文字を進めて間違いならその文字を赤色にして打ち終わったら終了を表示する

・テキストメッシュに代入してUnity上に表示する

ローマ字入力については以上の内容です。

 

かな入力の場合に関しては、ローマ字を生成するかわりに濁点と半濁点を分解する処理に差し替えるだけで済みます。例 が→か゛

一方で、ローマ字入力は様々な入力方法があるのでこれに対応するにはかなりの分量の辞書を用意して入力した文字と比較する必要があります。

更に、表示文字と違った場合には、打ったローマ字に変更する機能など、サンプルプロジェクトではこれらのすべての機能を実装してます。

 

処理の流れとしてはローマ字入力でもかな入力でも同じですが、これを簡単に分岐できる方法として、C#のクラスの継承とオーバーライドを使います。

覚えようと思うとたくさんの機能はありますが、タイピングゲームを作る際には以下の内容さえ理解できてれば十分です。

qiita.com

今回のサンプルプロジェクトでは、CTypeEngineクラスを基底クラスとして、CRomaTypeEngineとCKanaTypeEngine派生して、メソッドをオーバーライドしてやることによって、最初のインスタンス化の部分のみの変更ですべての処理を差し替えられるようにしてます。

CTypeEngine TypeEngine = new CRomaTypeEngine(); // ローマ字入力

CTypeEngine TypeEngine = new CKanaTypeEngine(); // かな入力

 

このたった一行の宣言だけで、ローマ字入力とかな入力の違いを吸収してくれるめちゃめちゃありがたい機能です!

これを使わないとやたらif分で分岐したり、同じ仕様の関数がたくさんできてしまったりと大変です。

後にきちんと説明しますが、基底クラス型の変数は派生クラス型でインスタンス化できるのです。

プロジェクトの構成の説明

シーンに追加されてるのは

Canvas下のViewTextMesh(TextMeshPro)とInputTextMesh(TextMeshPro)

・空のゲームオブジェクトを追加してコンポーネントとしてCGameManagerをアタッチ

以上の2点だけです。

もし自分で最初から作る場合は、このCGameManagerを使わずに自分でスクリプトを作ってどこのオブジェクトでもいいのでアタッチしても大丈夫です。

 

次にCTypeEngineとCRomaTypeEngineとCTypeKanaEngineの3つのスクリプトを自分で作ったプロジェクトにコピーします。

全体の処理の流れ

まず、Start関数でローマ字入力ならnew CRomaTypeEngine();でかな入力ならnew CKanaTypeEngine();でインスタンス化します。

MakeInputTextやMakeSearchStrはオーバーライドされた関数なので、この差し替えだけで関数の中身もすべてインスタンス化したクラスに定義した関数にさし変わります。

>|C#|
void Start()
{
TypeEngine = new RomaTypeEngine(); // ここをnew CKanaTypeEngine()にするとかな入力になる
string input_text = TypeEngine.MakeInputText(InputText[0]); // かな表示文章の1行目を入れる
Debug.Log(input_text);
foreach(var str in TypeEngine.MakeSearchStr(InputText[0], 0))
{
Debug.Log(str);
}
ViewTextMesh.text = ViewText[0];
InputTextMesh.text = input_text;
}
||<

ひらがなからタイピングする文章を生成する

MakeInputText関数は、ひらがなをワードを引数として入力するとローマ字入力用の文字かかな入力用の文字を生成してくれます。

例えば、

string input_text = TypeEngine.MakeInputText("であいとわかれのきせつ");

のように引数に「であいとわかれのきせつ」と入力すると「deaitowakarenokisetu」か「て゛あいとわかれのきせつ」とローマ字、かな、それぞれに対応したワードになります。

ひらがなからタイピングするワード候補を生成する

MakeSearchStrはこれからタイピングする文章の候補を生成します。

例えば、第一引数にひらがなのワード、第二引数に指定するのは、今打っている文字数です。

List<string> serach_texts = TypeEngine.MakeSearchStr("であいとわかれのきせつ", 0);

ローマ字入力の場合にserach_texts 入っている値は、「de」

かな入力の場合にserach_texts入ってる値は「W」と「w」でこれは「て」に当たるキーです。

List<string> serach_texts = TypeEngine.MakeSearchStr("であいとわかれのきせつ",10);

ローマ字入力の場合にserach_texts 入っている値は、「tu」と「tsu

かな入力の場合にserach_texts入ってる値は「P」「p」でこれは「せ」に当たるキーです。かな入力の場合は、濁点と半濁点を分解した文字数になるので、10文字目は「せ」になるのに注意が必要です。

 

最期に生成したワードをTextMeshProに代入して表示させます。

        ViewTextMesh.text = ViewText[0];
        InputTextMesh.text = input_text;

ここまでで、すべての初期化処理が終了して、最初の文章が表示されてる状態になります!

ここまでで重要なのは、MakeInputTextでひらがなのワードから文章を生成して、MakeSearchStrはひらがなのワードから、これから入力する文章を生成するということしです。

中身についても今後、解説する予定ですが、使い方さえわかっていれば、タイピングゲームは十分に作れます。

入力処理

取得できないキーについて

Unityでキーボードの入力を取得するには、Input.GetKeyDownなどを使用するのが一般的ですが、日本語キーボードを使うと判定できないキーが存在します。

解決方法はTwitterで教えてもらえたので本当に助かりました!もしかしたら、Unityでかな入力のタイピングゲームが少ないのはこれが原因なのでは!?

ということで、この点を考慮するとOnGUI()で入力したキーを取得して、それを元にMakeSearchStrで生成したワードと比較して、正解なら正解の処理を間違いなら間違った時の処理を記述していきます。

シフトキー判定

まずは、Update関数でシフトキーの入力の有無を取得しておきます。

>|C#|     
bool LeftShift = false, RightShift = false;
void Update()
{
  if (Input.GetKeyDown(KeyCode.LeftShift))
  {
      LeftShift = true;
  }
  if (Input.GetKeyUp(KeyCode.LeftShift))
  {
      LeftShift = false;
  }
  if (Input.GetKeyDown(KeyCode.RightShift))
  {
      RightShift = true;
  }
  if (Input.GetKeyUp(KeyCode.RightShift))
  {
      RightShift = false;
  }
}
||<

シフト以外のすべてのキーはOnGUIで取得して、TypeEngine.Keypressに渡すとタイピングする文章が全文章打ち終わったかどうか判定して、打ち終わったらtrue、打ち終わってなければ、falseが返ってきます。

 

Event.current.type == EventType.KeyDownと書けば、キーが押された時のイベントを取得できますが、キーを話した時やキーコードが無効な時は弾くようにしています。

>|C#|
private void OnGUI()
{
Event e = Event.current;
  if (e.type == EventType.KeyDown && e.type != EventType.KeyUp && e.keyCode != KeyCode.None
        && e.keyCode != KeyCode.LeftShift && e.keyCode != KeyCode.RightShift)
  {
      if (TypeEngine.KeyPress(InputText, e, LeftShift || RightShift))
       {
            ViewTextMesh.text = "終了";
            InputTextMesh.text = "";
       }
       else
       {
           // 漢字文字を表示させる
           ViewTextMesh.text = ViewText[TypeEngine.TextPos];
           // すでに打ち終わった文字
           string str1 = "<color=#808080>" + TypeEngine.NowInputText.Substring(0, TypeEngine.InputPos) + "</color>";
           // これから打つ1文字
           string str2 = string.Empty;
           if (TypeEngine.TypeMissFlag)
           {
               str2 = "<color=#FF0000>" + TypeEngine.NowInputText.Substring(TypeEngine.InputPos, 1) + "</color>";
           }
           else
           {
               str2 = "<color=#0000FF>" + TypeEngine.NowInputText.Substring(TypeEngine.InputPos, 1) + "</color>";
           }
           // これから打つ1文字より後ろの最期の文字まで
           string str3 = "<color=#FFFFFF>" + TypeEngine.NowInputText.Substring(TypeEngine.InputPos + 1) + "</color>";
           // 入力文字に反映させる
           InputTextMesh.text = str1 + str2 + str3;
     }
  }
}
||<

 

このKeyPressで渡しているInputTextは、先ほど説明したMakeInputTextやMakeSearchStrで生成する前の文章を渡しています。

理由は初期化以降のMakeInputTextやMakeSearchStrはKeyPress関数の内部で呼ばれているからです。

あまり内部と外部の両方で呼べるようにするのは、設計としてはよくないのですが、この2つの関数の使い方を理解するのが、タイピングゲームを作る上で非常に重要なことなので、説明のために外部からも呼べるようにpublicにしています。

TextMeshProへの表示

あらかじめUnityのエディタ側からアタッチして用意しておいたViewTextMeshとInputTextMeshにTypeEngine.KeyPressで処理した結果を参照して文字の色を変えたりして表示させます。

漢字文字を表示させるのは簡単で以下の一行で済みます。(TextPosは何行目の文章を売っているかの行数が入っている)

                // 漢字文字を表示させる
                ViewTextMesh.text = ViewText[TypeEngine.TextPos];

次にタイピングした文字を表示されるのですが、この時に打ち終わった色とこれから打つ色とそれより後ろの文字の色の3種類の色分けをします。

更にこれから打つ色を不正解の時には赤色に変更してミスしたのをわかりやすくします。

文字の色を一文字ずつ変えるには<color>タグを使ってRGBを16進数で指定して色を変えたい文字を挟むとその色に変更できます。

sunagitsune.com

実行してみるとタイピングして正解の時は文字が進んで間違いの時は赤文字で表示されるのを確認できます。

gyazo.com

また、先ほど説明した通り、ローマ字入力とかな入力の変更はStart関数内の

TypeEngine = new CRomaTypeEngine();

の一行を

TypeEngine = new CKanaTypeEngine();

としてやるだけで簡単に変更できます。

ここまででローマ字入力だけでなくかな入力にも対応しているタイピングゲームができました。ちなみにこの時点でローマ字入力の柔軟なうちわけにも対応しています。例えば「し」だったら「si」でも「shi」でもどっちでも打ち分けられます。

「っ」を「xtu」や「ltu」などにも対応しているので色々と打ち分けを試してみましょう!

まとめ

今回、学んだことをまとめると

1.ひらがなからタイピング文章を生成する(MakeInputStr)

2.ひらがなから入力判定するのに必要な文字を作る(MakeSearchStr)

3.キー入力から正誤判定をして正解なら文字を進めて間違いならその文字を赤色にして打ち終わったら終了を表示する

4.テキストメッシュに代入してUnity上に表示する

5.ローマ字入力とかな入力の切り替え

大きくわけて以上の5つになります。

この処理はUnityでタイピングゲームを作ろうと思ったら必要になるので、覚えておきましょう。

今回はTypeEngineの使い方のみ説明したので次回以降はTypeEngineのスクリプト内についても説明する予定です。

最終的にはUIで様々なパラメーターを変更してプレイヤーが好みの状態でタイピングゲームをプレイできるところまで作ります


それでは、次回のタイピングゲーム作り方講座で会いましょう!

 

「0から配信で作るVtuber計画」で魅力的なVtuberモデルを作るアイデアを考えてみた

こんにちは!ジェイです。

今回は、今時にVtuberモデル作るにあたって魅力的なモデルを作るにはどうしたらいいのか?というお題を考えてみます。

ただし、私はプログラミングはできますが、モデリングや絵を描いたりはできないので、あくまで企画のお話になります。

魅力的なVtuberモデルを作るのを考えたきっかけ

現役Vtuberの私が、なぜこれを考えようと思ったかというと犬甘(いぬかい)みかんさんの以下の「0から配信で作るVtuber計画」というのが非常におもしろそうで興味を持ったからです。

実際に魅力的なVtuberモデルを作るにはどうしたらいいか?

すでにVtuberは2万人以上いて、デビューした有名なVtuberのほとんどは有名な絵師さんや知り合いに頼むことが多いです。

実際に私もcoconaraで3Dモデルの制作を依頼したことがあります。

 

そんなプロ達とは違う魅力を出して個性的なVtuberのモデルを作るにはどうしたらいいでしょうか?

結論からいうと「誰にどんな体験をさせたいか?」というのが一番大事です。(ここでの対象はモデルを使用するVやそのファンのみなさん)

 

Vtuberとして活動するようになるとモデルの世界観、そしてその世界観の中で何か自分なりの目標に向かっていこうというのがVtuber活動だと思います。

つまり、Vtuberとしてどんな世界観でどんなキャラクターでどんな活動をするか?がセットになって最大限の魅力が発揮されることになります。

 

とはいえ、この点は魂が確定して方針まで決まっている場合についての話なので、今回の配信をしながらリスナー達と「0から配信で作るVtuber計画」では、これを意識しつつも、別のアプローチを考えてみます。

 

みかんさんの配信では、「VTuberのモデルをどんなビジュアルにするか?」などはたくさんアイデアが出ていたので、もう1つ大事な「誰に対して作るのか?」について考えてみます。

誰に対してVtuberモデルを作るのか?

これを決めるにはまず、犬甘(いぬかい)みかんの配信に集まるリスナーの属性を考えてみます。

www.youtube.com

コメント欄を見てると見事にゲーム制作をしているリスナーさんたちが集まっています。私の知ってる人も何人かいてリスナーさんの共通点はUnrealEngineを使ってゲーム制作を行っている人が多いということです。

ゲーム制作,UnrealEngneを擬人化したモデル

現代ゲーム制作を行う上で必須になってくるのがゲームエンジンで多くのゲーム制作者「Unity」と「UnrealEngine」を使っています。

Unityにはunitychanという無償で3Dモデルが配布されています!

unity-chan.com

そのおかげで、Unityユーザーは自作ゲームに簡単に美少女モデルを取り込むことができるし、私自身もunitychanには何度もお世話になりました。

 

で一方でUnrealEngineはというと・・・

Unrealちゃんがいないだと!!

 

そうなんです。unitychanがいる一方でUnrealちゃんはいないんですよね…

これは何とか擬人化できないだろうか?という考えが出てきます。

 

最近だとVTuberは擬人化するのが一般的で実はこの方法は以下の様なメリットが存在します。

・商品や地名を擬人化することで販売促進などできる

VTuber知名度が上がると元の商品や地名の知名度が上がる

・逆に有名な商品や地名の擬人化モデルを作れると最初からある程度有名で個性が出る

これらのメリットがある上に、擬人化したVTuberモデルは強烈に個性が出るという特徴があります。

 

現在2万人もいるVTuberの中で似たようなモデルを見ることはよくありますが、擬人化したVTuberは強力な個性が出るので、モデル自体が多少似ていてもあのモデルと被っているとなると思う人も減るでしょう。

具体的なモデルの内容

ここまででUnrealEngineの擬人化が良いのでは?という主張をしてきました。

しかし、問題なのがUnrealEngineの権利はEpic Gamesが持っているので、公式として堂々と活動することはできません。

ただ、この辺りを考えてモデルのアイデアを作ること自体は悪くないと思っています。

 

例えば、リスナーさんにUnrealちゃんっているとしたらどんなイメージか?と聞いてみるのもおもしろそうです。

具体的には

・どんな色か?

・何歳くらいか?

・和風か洋風か?

・どんな服を着てそうか?

など、個人の勝手なイメージでいいので集計するのも、作業をする上での叩き台になると思います。

もちろんUnrealちゃんとしましたが、そうでなくてもよくて、なるべくリスナーの属性に合わせた方がメリットが多いというだけで、他の擬人化などでも効果は期待できます。なので、ゲーム制作という大きな枠組みでワードを考えてみるのもありだと思います。

まとめ

今回は、VTuberモデルの魅力の出し方に擬人化という方法を述べて、企画のターゲットはゲーム制作者がいいのではないか?という提案をしました。

すでにラフ案のモデルはいくつか出来ていて下記の様なスケジュールで進めていく予定らしいです。

また、完成したモデルは無償でお渡ししますとのことなので、この機会にVTuberになりたいと思った方は応募してみてはいかがでしょうか!(ピンク髪JK美少女モデルができたら私がなりたいくらい)

ちなみに私が一番気に入ってるモデルは美少女TPSゲームに出ているこの娘がめちゃ大好きです。この瞳に見つめられるとドキッとしますよね。

引き続きゲーム制作も進行していくらしいので、ぜひチャンネル登録して応援していきましょう(^^♪

I.M.W - YouTube

 

個人ゲーム制作者に朗報!定番配信ゲームの募集

こんにちは!ジェイです。

早くも2024年の2月が終わってしまいましたが、みなさんは新年度に立てた目標は達成できそうですか?

私はゲーム制作の展覧会を開きたいという大きな目標を立てましたが、いつ達成できるのか?という不安はありますが一歩ずつ進めていく予定です。

 

そして、今回は個人ゲーム制作者様に、とても関係ある「定番配信ゲーム」を募集することにしました!

特にオンラインのみで活動している個人ゲーム制作者様にとっては、良い話になると思います。

定番配信ゲームとは?

現在、私はYouTubeで毎週土曜日に20時か21時にゲーム実況配信をしています。特にくろあカートがお気に入りで毎回プレイするごとに進化してる上にマルチプレイで同時にできるので、特に配信向きなゲームですね!

 

主に私の好きなレトロゲームや個人ゲーム制作者さんのゲームを実況プレイすることにしています。

 

しかし!!ここで大きな問題があって、毎週プレイするゲームを何にしようか?という問題で迷っています。

 

今は世の中ゲームで溢れていて、毎回誰のどんなゲームをプレイすればいいか?というので迷った結果、元々知ってるゲームや知ってる人が作ったゲームをプレイするような形で落ち着いています。

 

だけど、実は世の中にはこんなおもしろいゲームがあって、個人ゲーム制作者がいたのかというおどろきを感じたいのも事実です。

 

そこで思いついたのは、毎回実況するゲームをある程度絞ってから、そのゲームを何回も配信で実況プレイするスタイルを取りたいと思いました。

 

理由は、このスタイルだと、個人ゲーム制作者にも実況する私にもメリットがあってお互いにwinwinな関係になれるからです。

個人ゲーム開発者側のメリット

私自身がゲーム制作者であるという立場をフルに利用してメリットを提供します。

 

今回の募集するゲーム制作者さまに対してのメリットをまとめると

1.自分の作ったゲームを定期的に実況プレイしてもらえる

2.とにかく褒めまくるのでモチベーションアップにも繋がる

3.ゲームの規模や完成度は問わないので初心者にもチャンスがある

4.紹介するディスコードのメンバーはUnityやUnrealEngineを使っているゲーム制作者なので交流が深められる

5.配信する人がFC世代の超ド級のゲームヘビーユーザーなので、もしはまったらそのゲームを一生やり続ける

 

1.「自分の作ったゲームを定期的に実況プレイしてもらえる」

多くの個人ゲーム制作者が持っている問題として、自分のゲームどうやって宣伝するか?どうやってプレイしてもらうか?ということがありますよね。

 

そこで、あなたが作ったゲームを配信でプレイしてもらっているところをイメージしてみてください。

 

確かにたくさんのユーザーにゲームをやってもらうのは大事なことです。

しかし、そのプレイしてるところを実際に見られる機会ってどれくらいあるでしょうか?

 

よほど、有名なゲームでもない限りは中々自分の作ったゲームを実況プレイしてもらえるような機会は少ないはずです。

 

ですが、今回募集する定番配信ゲームに採用されたのなら、定期的にゲームを実況してもらえる権利を得ることができます。

 

あなたは、「プレイしてるところをまったく想像できないたくさんのユーザー」と「プレイしているところが配信で見えるユーザー」はどっちに価値があると思いますか?

 

もちろん、この答えは人それぞれなのですが、この企画は自分のゲームを配信してもらいたいという人にはうってつけなんです。

2.「とにかく褒めまくるのでモチベーションアップに繋がる」

ゲーム制作をする上でどうしてもモチベーションが上がらない。という悩みをもったことはありませんか?

確かに、自力で毎日できるように工夫するのも大事なことですが、ゲーム制作を続けるうえで大事なことは、一定の期間に他人からのフィードバックをもらうことです。

 

特に始めたばかりの頃は、SNSをやっていても中々反応をもらえなくてやる気がなくなってしまうこともありますよね。

Twitterなどでゲーム制作の進捗を上げると反応してもらえると嬉しいですし、とても励みになるでしょう。

 

ゲーム制作をする段階で最初から、Twitterのフォロワー数が多かったり、YouTubeである程度再生数を稼げるような人は、反応をもらったり褒めてもらったりされやすいのでそんなに気にすることではないと思います。

 

しかし、これから上を目指す人は、そのために毎日注目を集めるようなツイートをしたり、毎週YouTubeで動画を編集をしてアップしなければなりません。

これってゲーム制作をしてる人からすると自分のゲームを作りながら、宣伝もしなければならないのですごく大変なとこですよね。

 

もし今回の募集する定番配信ゲームに使う事が決まったら、その宣伝の部分が改善されるし、作ったゲームを褒めまくるのでモチベーションの維持にもなるでしょう。

これはゲーム制作の初心者ほど特に大事だと思います。

こんな人に応募してもらいたい

まず、個人ゲーム制作者がゲームを完成させた後には、何かしらのイベントに応募しますよね?それを目標に頑張っている人も多いと思います。

 

そして、そのオンラインで開催してるイベントを並べてみると

ニコニコ自作ゲームフェス

unity1week

ぷちコン

新人フリーゲームコンテスト

 

オフラインでは、以下のような有名なコンテストがたくさん開催されています。

個人ゲーム制作で気にしておきたいコンテスト、イベントまとめ | えきふるゲームラボ

当然、これらのイベントで良い成績を収めれば、注目を集められるので、必然と遊んでくれるユーザーが増えるでしょう。

 

ただし、考えてもらいたいのが、このコンテストで遊んでくれるユーザーがライトユーザーかヘビーユーザーか?ということです。

前回の記事で、個人ゲーム制作者がヘビーユーザーとライトユーザーどちらを狙うべきか?というのを解説しました。

happynetwork2019.hatenablog.com

 

この記事を読むと現在のライトユーザーは、1990年代に比べて圧倒的に早くゲームを飽きてしまうという衝撃の事実に気づけます。

 

そして結論で、ゲームをすぐにやめてしまうライトユーザーとずっとやり続けるヘビーユーザーのどちらをターゲットにするべきか?という問いに、私は「ヘビーユーザー」をターゲットにすべきという答えを出しました。

 

ここでようやく今回の「定番配信ゲームの募集」と関係のある話ができるようになりました。

 

私自身はFC時代からずっとゲームやっているヘビーユーザーです。事実として、歌謡タイピング劇場などのタイピングゲームは17年間もやっています。

 

そんなヘビーユーザーの私があなたの作ったゲームを定期的にしかも無料で配信すると言ったらどうでしょうか?

 

私自身もこのゲームをおもしろくするにはどう配信を見せたらいいだろうか?どう修正したらもっとおもしろくなるだろうか?というのを考えながらやっていきます。

 

これだけの条件を揃えたヘビーユーザーの私が、あなたのゲームを定期的に配信すると言ってるのですから、このチャンスをぜひ生かしてみませんか。

応募条件

無料で応募可能と言ったものの、やはり配信をするには自分の好みにあったものでないと定期的にはできないので、以下の条件を付けさせていただきます。

ネタバレに強いゲーム

定期的にやりたいので、一度見てしまったらやる気がなくなるタイプのゲームはあまり適してないです。

ノベルゲームやRPG以外のジャンル

これもジャンル的にどうしてもネタバレに弱くなってしまうのと、個人的にノベルゲームを配信するのは、読まないといけないとか考えてしまって、かなり苦手なので申し訳ないですが、ノベルゲームは採用できないです。

RPGは、何度もやっても大丈夫な工夫やアクション要素が入ってるものは大丈夫です。

 

その他にあったらいいなと思う要素についていうと

・定期的に更新してくれるゲーム

・オンラインでのマルチプレイやランキング要素

この2つは絶対ではないものの配信することを考えたら、あった方がすごく有利です。

 

もちろん、unity1weekなどのイベントに出した後のゲームでも現在未完成の作品でも構いません。

初心者でもある程度形になっているゲームがあれば応募可能です。

応募方法

まずは、以下からdiscordに入っていただいて、自己紹介を書くか、もしくはTwitterなどプロフィールや作ってるゲームの内容がわかるものをdiscordの自己紹介に張り付けてください。その時点で応募完了です。

discord.gg

もちろん何となくゲーム制作に興味はあるけど、よくわからないという人も参加OKです

配信サイクルや選考について

定番配信ゲームの配信サイクルについてですが、多くて1カ月に1回で少なくとも2カ月に1回を予定してます。

配信自体はYouTubeで、土曜日の20時か21時から1時間やる予定です。

 

とりあえず、1回配信して見た後に続けられるかどうか?を考えてから大丈夫そうなら「定番配信ゲーム」として定期的に配信することにします。

 

まとめ

ゲーム制作者が、定期的に自分の作ったゲームを配信してもらえることにメリットを感じる人も多いでしょう。

ですが、ゲーム制作初心者やオンライン限定で活動している人などは、その機会が訪れるのは難しいと思います。

 

今回の企画は、そんなゲーム制作者さんとwinwinな関係を結べたらいいなと思って考えました。

あなたがもし定番配信ゲームをしてほしいと少しでも思うなら、応募していただけたら嬉しいです。ここまで読んでくれたみなさんありがとうございました。

そして、配信するチャンネルは、以下なのでよかったら登録してくれたら嬉しいです。

www.youtube.com