2021-03-06に投稿

Unityで複数の入力方法に対応するタイピングゲームを作ろう

読了目安:21分

はじめに

はじめまして、情報系の大学に通っているプログラム初心者の大学生です。

2年前にゼミ内で小さなゲームジャムが開催され、タイピングゲームを作ることになったのですが、ネット上で紹介されている既存のタイピングゲームの制作方法では入力方法に以下のような問題があると思いました。

1. 単語を打つ手段があらかじめ決まっている。
例 : "し"と打つときに"si"と打たなければいけなく、"shi"には対応していない。

2.単語を別々に入力できない。
例 : "ちゃ"と打つときに"ち"と"ゃ"を別々にタイプできない。

そこで今回はどんな打ち方にも対応できるタイピングゲームの制作に挑戦してみました。
この記事では仕組みとプログラムに関して紹介していきます、参考にしていただければ幸いです。

参考にした作品

今回制作したプログラムでは「寿司打」のタイピングシステムを参考にしています。

寿司打

こちらからプレイできます。

どのようなシステムかというと

キャプチャ1.PNG
ゲーム画面ではこのようにタイプする文字と入力方法が表示されています。
この状態で"し"を"si"ではなく"shi"と入力しようとすると...

キャプチャ3.PNG
"sh"と入力した時点で1つ目の"し"の入力方法が"shi"に置き換わります。
(2つ目の"し"の入力方法は変わらず表示されている。)

他にも

キャプチャ4.PNG
"ック"を入力する際に、"xtu"と"ku"で別々に入力しようとすると...

キャプチャ5.PNG
"xtu"の"x"を入力した時点で”ッ”と"ク"を別々と考えて入力方法を置き換えています。

プログラム

筆者なりに上記のシステムを再現しようとすると以下のようなプログラムになりました。

Kanji.txt

タイピングゲーム内で使う文章をそのまま入力します。
一度のタイピングで入力する長さを決めて改行してください。

別に君を求めてないけど
横にいられると思いだす。
君のドルチェ&ガッパーナの
その香水のせいだよ。

Japanese.txt

Kanji.txtの内容を全てひらがなにして入力してください。

べつにきみをもとめてないけど
よこにいられるとおもいだす。
きみのどるちぇあんどがっぱーなの
そのこうすいのせいだよ。

KanjiFuri.txt

Kanji.txt内の文字をひらがなにした時、何文字になるかを入力してください(ひらがなの場合は1)

21212111111
211111121111
2111113111111
1122111112

Dictionary.cs

全ての入力方法を連想配列で整理しています。
下の方にある関数はKeyかValueを渡すことで紐づけられた要素を全て取り出すことができます

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Dictionary : MonoBehaviour
{
    //デフォルトの入力方法
    public readonly Dictionary<string, string> dic = new Dictionary<string, string>() {

        {"あ", "a"},{"い", "i"},{"う", "u"},{"え", "e"},{"お", "o"},
        {"か", "ka"},{"き", "ki"},{"く", "ku"},{"け", "ke"},{"こ", "ko"},
        {"さ", "sa"},{"し", "si"},{"す", "su"},{"せ", "se"},{"そ", "so"},
        {"た", "ta"},{"ち", "ti"},{"つ", "tu"},{"て", "te"},{"と", "to"},
        {"な", "na"},{"に", "ni"},{"ぬ", "nu"},{"ね", "ne"},{"の", "no"},
        {"は", "ha"},{"ひ", "hi"},{"ふ", "hu"},{"へ", "he"},{"ほ", "ho"},
        {"ま", "ma"},{"み", "mi"},{"む", "mu"},{"め", "me"},{"も", "mo"},
        {"や", "ya"},{"ゆ", "yu"},{"よ", "yo"},
        {"ら", "ra"},{"り", "ri"},{"る", "ru"},{"れ", "re"},{"ろ", "ro"},
        {"わ", "wa"},{"を", "wo"},{"ん", "n"},
        {"が", "ga"},{"ぎ", "gi"},{"ぐ", "gu"},{"げ", "ge"},{"ご", "go"},
        {"ざ", "za"},{"じ", "zi"},{"ず", "zu"},{"ぜ", "ze"},{"ぞ", "zo"},
        {"だ", "da"},{"ぢ", "di"},{"づ", "du"},{"で", "de"},{"ど", "do"},
        {"ば", "ba"},{"び", "bi"},{"ぶ", "bu"},{"べ", "be"},{"ぼ", "bo"},
        {"ぱ", "pa"},{"ぴ", "pi"},{"ぷ", "pu"},{"ぺ", "pe"},{"ぽ", "po"},
        {"ぁ","xa" },{"ぃ","xi" },{"ぅ","xu" },{"ぇ","xe" },{"ぉ","xo" },
        {"っ", "xtu"},
        {"ゃ","xya" },{"ゅ","xyu" },{"ょ","xyo"},
        {"きゃ","kya"},{"きぃ","kyi"},{"きゅ","kyu"},{"きぇ","kye"},{"きょ","kyo"},
        {"しゃ","sya"},{"しぃ","syi"},{"しゅ","syu"},{"しぇ","she"},{"しょ","syo"},
        {"ちゃ","tya"},{"ちぃ","tyi"},{"ちゅ","tyu"},{"ちぇ","tye"},{"ちょ","tyo"},
        {"にゃ","nya"},{"にぃ","nyi"},{"にゅ","nyu"},{"にぇ","nye"},{"にょ","nyo"},
        {"ひゃ","hya"},{"ひぃ","hyi"},{"ひゅ","hyu"},{"ひぇ","hye"},{"ひょ","hyo"},
        {"みゃ","mya"},{"みぃ","myi"},{"みゅ","myu"},{"みぇ","mye"},{"みょ","myo"},
        {"りゃ","rya"},{"りぃ","ryi"},{"りゅ","ryu"},{"りぇ","rye"},{"りょ","ryo"},
        {"ぎゃ","gya"},{"ぎぃ","gyi"},{"ぎゅ","gyu"},{"ぎぇ","gye"},{"ぎょ","gyo"},
        {"じゃ","zya"},{"じぃ","zhi"},{"じゅ","zyu"},{"じぇ","zye"},{"じょ","zyo"},
        {"ぢゃ","dya"},{"ぢぃ","dyi"},{"ぢゅ","dyu"},{"ぢぇ","dye"},{"ぢょ","dyo"},
        {"びゃ","bya"},{"びぃ","byi"},{"びゅ","byu"},{"びぇ","bye"},{"びょ","byo"},
        {"てゃ","tha"},{"てぃ","thi"},{"てゅ","thu"},{"てぇ","the"},{"てょ","tho"},
        {"うぁ","wha"},{"うぃ","whi"},{"うぇ","whe"},{"うぉ","who"},
        {"でゃ","dha"},{"でぃ","dhi"},{"でゅ","dhu"},{"でぇ","dhe"},{"でょ","dho"},
        {"くぁ","qa"},{"くぃ","qi"},{"くぇ","qe"},{"くぉ","qo"},
        {"ふぁ","fa"},{"ふぃ","fi"},{"ふぇ","fe"},{"ふぉ","fo"},
        {"ヴぁ","va"},{"ヴぃ","vi"},{"ヴ","vu"},{"ヴぇ","ve"},{"ヴぉ","vo"},
        {"ぴゃ","pya"},{"ぴぃ","pyi"},{"ぴゅ","pyu"},{"ぴぇ","pye"},{"ぴょ","pyo"},
        {"、","," },{"。","."},{"「","["},{"」","]"},
    };

    //デフォルトではない入力方法
    public readonly Dictionary<string, string> Epicdic = new Dictionary<string, string>()
    {
        {"ca","か" },{"ci","し" },{"cu","く" },{"ce","せ" },{"co","こ" },
        {"cha","ちゃ"},{"chi","ち"},{"chu","ちゅ"},{"che","ちぇ"},{"cho","ちょ"},
        {"cya","ちゃ"},{"cyi","ちぃ"},{"cyu","ちゅ"},{"cye","ちぇ"},{"cyo","ちょ"},
        {"fu","ふ"},
        {"ja","じゃ"},{"ji","じ"},{"ju","じゅ"},{"je","じぇ"},{"jo","じょ"},
        {"la","ぁ" },{"li","ぃ" },{"lu","ぅ" },{"le","ぇ" },{"lo","ぉ" },
        {"lya","ゃ" },{"lyu","ゅ" },{"lyo","ょ" },
        {"ltu", "っ"},
        {"nn","ん" },
        {"qu","く" },{"qyi","くぃ"},{"qye","くぇ"},
        {"sha","しゃ" },{"shi","し"},{"shu","しゅ"},{"she","しぇ"},{"sho","しょ"},
        {"tsu","つ"},
        {"yi","い"},{"ye","え"},
    };

    //渡した要素と紐づいている要素をdicから探し、リストにして返します。
    public KeyValuePair<string, string> SearchdicKey(string key)
    {
        var dicpair = new KeyValuePair<string, string>();

        foreach (KeyValuePair<string, string> pair in dic)
        {
            if (key == pair.Key)
            {
                dicpair = pair;
            }
        }
        return dicpair;
    }

    //渡した要素と紐づいている要素をdicから探し、リストにして返します。
    public KeyValuePair<string, string> SearchdicValue(string value)
    {
        var dicpair = new KeyValuePair<string, string>();

        foreach (KeyValuePair<string, string> pair in dic)
        {
            if (value == pair.Value)
            {
                dicpair = pair;
            }
        }
        return dicpair;
    }

     //渡した要素と紐づいている要素を両方のdicから探し、リストにして返します。
    public List<KeyValuePair<string, string>> SearchTotaldicKey(string key)
    {
        var dicpair = new List<KeyValuePair<string, string>>();

        foreach (KeyValuePair<string, string> pair in Epicdic)
        {
            if (key == pair.Value)
            {
                dicpair.Add(new KeyValuePair<string, string>(pair.Value, pair.Key));
            }
        }

        foreach (KeyValuePair<string, string> pair in dic)
        {
            if (key == pair.Key)
            {
                dicpair.Add(pair);
            }
        }
        return dicpair;
    }

    //渡した要素と紐づいている要素を両方のdicから探し、リストにして返します。
    public List<KeyValuePair<string, string>> SearchTotaldicValue(string value)
    {
        var dicpair = new List<KeyValuePair<string, string>>();

        foreach (KeyValuePair<string, string> pair in Epicdic)
        {
            if (value == pair.Value)
            {
                dicpair.Add(pair);
            }
        }

        foreach (KeyValuePair<string, string> pair in dic)
        {
            if (value == pair.Key)
            {
                dicpair.Add(new KeyValuePair<string, string>(pair.Value, pair.Key));
            }
        }
        return dicpair;
    }
}

SetList.cs

次にタイピングするKanjij.txtと同じ行にあるJapanese.txtとKanjiFuri.txtの文字列をSetLists()に渡し、それぞれを区分しながらListに要素を追加していきます。

・Japanese.txtは一度に入力可能でありながら、文字列が最長になるように区分し、FuriListに追加していきます。
・KanjiFuri.txtは1つの数字ごとに文字列を区分しながらKanjiFuriListに追加していきます。
・また、FuriListに要素を追加しながらDictionary.dicから入力方法を取得し、RomaListに追加していきます。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class SetList : MonoBehaviour
{
    private SetText ST;
    private Dictionary Dic;

    public List<string> RomaList;
    public List<string> FuriList;
    public List<int> KanjiFuriList;

    void Awake()
    {
        ST = GetComponent<SetText>();
        Dic = GetComponent<Dictionary>();
    }

    private string FuriStr = "";
    private string RomaStr = "";

    //FuriStrとRomaStrの内容をそれぞれのリストに入れて初期化する。
    private void AddList()
    {
        FuriList.Add(FuriStr);
        RomaList.Add(RomaStr);

        FuriStr = "";
        RomaStr = "";
    }

    //渡されたJapanese.txtの一行を一文字づつ仕分けるよう
    public void SetLists(string Roma , string KanjiFuri)
    {

        for(int i = 0; i < Roma.Length - 1; i++)
        {
            var chr = Roma[i].ToString();

            switch (chr)
            {
                case "っ":

                    FuriStr += chr;

                    if (i != Roma.Length - 1)
                    {
                        switch(Roma[i + 1].ToString())
                        {
                            case "あ":
                            case "い":
                            case "う":
                            case "え":
                            case "お":
                            case "な":
                            case "に":
                            case "ぬ":
                            case "ね":
                            case "の":
                            case "ん":
                                RomaStr += Dic.SearchdicKey(Roma[i + 1].ToString()).Value;
                                AddList();
                                break;

                            default:
                                RomaStr += Dic.SearchdicKey(Roma[i + 1].ToString()).Value.Substring(0,1);
                                break;
                        }
                    }
                    else
                    {
                        FuriStr += chr;

                        AddList();
                    }
                    break;

                case "ぁ":
                case "ぃ":
                case "ぅ":
                case "ぇ":
                case "ぉ":
                case "ゃ":
                case "ゅ":
                case "ょ":

                    if (i != 0 && FuriStr != "")
                    {

                        if (Dic.dic.ContainsKey(FuriStr + chr))
                        {
                            FuriStr += chr;

                            RomaStr += Dic.SearchdicKey(Roma[i - 1].ToString() + chr).Value;

                            AddList();
                        }
                        else
                        {
                            AddList();
                            FuriStr += chr;
                            RomaStr += Dic.SearchdicKey(chr).Value;
                            AddList();
                        }
                    }
                    else
                    {
                        FuriStr += chr;

                        RomaStr += Dic.SearchdicKey(chr).Value;

                        AddList();
                    }

                    break;

                case "ん":

                    FuriStr += chr;
                    RomaStr += Dic.SearchdicKey(chr).Value;

                    if (i != Roma.Length - 1)
                    {
                        switch(Roma[i + 1].ToString())
                        {
                            case "あ":
                            case "い":
                            case "う":
                            case "え":
                            case "お":
                            case "な":
                            case "に":
                            case "ぬ":
                            case "ね":
                            case "の":
                            case "ん":
                            case "や":
                            case "ゆ":
                            case "よ":
                            case "ゃ":
                            case "ゅ":
                            case "ょ":
                                RomaStr += "n";
                                break;
                        }
                    }
                    else
                    {
                        RomaStr += "n";
                    }

                    AddList();

                    break;

                default:

                    if (i != Roma.Length - 1)
                    {
                        switch (Roma[i + 1].ToString())
                        {
                            case "ぁ":
                            case "ぃ":
                            case "ぅ":
                            case "ぇ":
                            case "ぉ":
                            case "ゃ":
                            case "ゅ":
                            case "ょ":

                                FuriStr += chr;

                                break;

                            default:
                                FuriStr += chr;
                                RomaStr += Dic.SearchdicKey(chr).Value;

                                AddList();
                                break;
                        }
                    }
                    else
                    {
                        FuriStr += chr;
                        RomaStr += Dic.SearchdicKey(chr).Value;

                        AddList();
                    }


                    break;
            }
        }

        int num = 0;

        for(int i = 0; i < KanjiFuri.Length - 1; i++)
        {
            num += KanjiFuri[i] - '0';

            KanjiFuriList.Add(num);
        }
    }
}

SetText.cs

txtファイルや表示中の文字列の行数、打ち終わった字数などを管理しています。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;


public class SetText : MonoBehaviour
{
    private SetList SL;
    private TypingSystem TS;

    [SerializeField] private TextAsset Furigana = null;     //ここにKanjiFuri.txtを入れる
    [SerializeField] private TextAsset Hiragana = null;     //ここにJapanese.txtを入れる
    [SerializeField] private TextAsset Kanji = null;        //ここにKanji.txtを入れる

    [SerializeField] private List<string> FuriList = new List<string>();
    [SerializeField] private List<string> HiraList = new List<string>();
    [SerializeField] private List<string> KanjiList = new List<string>();

    [SerializeField] private Text RomaText = null;  //ここにKanjiFuri.txtの内容を表示するためのテキストを入れる
    [SerializeField] private Text FuriText = null;  //ここにJapanese.txtの内容を表示するためのテキストを入れる
    [SerializeField] private Text KanjiText = null; //ここにKanji.txtの内容を表示するためのテキストを入れる

    [SerializeField] private int ColumunNum = -1; //現在打っている文字列の行数、最初に+1するため最初は-1にする

    private int SetColumunNum
    {
        get { return ColumunNum; }
        set
        {
            ColumunNum = value;
            SL.SetLists(HiraList[ColumunNum],FuriList[ColumunNum]);
            KanjiProp = 0;
            ListProp = 0;
            FuriProp = 0;
            RomaProp = 0;
        }
    }

    public int Romanum { get; private set; }
    public int Furinum { get; private set; }
    public int Kanjinum { get; private set; }
    public int Listnum { get; private set; }

    //メインの文字の設定
    private int KanjiProp
    {
        get { return Kanjinum; }
        set
        {
            Kanjinum = value;
            var kanjifuristr = "<color=\"red\">";
            kanjifuristr += KanjiList[ColumunNum].Insert(Kanjinum, "</color>");
            KanjiText.text = kanjifuristr;
        }
    }

    //フリガナの文字の設定
    private int FuriProp
    {
        get { return Furinum; }

        set
        {
            Furinum = value;
            var furistr = "<color=\"red\">";
            furistr += HiraList[ColumunNum].Insert(Furinum, "</color>");
            FuriText.text = furistr;

            while (Furinum >= SL.KanjiFuriList[Kanjinum])
            {
                KanjiProp++;
            }
        }
    }

    //SetList.FuriListの何個目の要素を打っているか
    private int ListProp
    {
        get { return Listnum; }

        set
        {
            if(value == SL.FuriList.Count)
            {
                ResetText();
                return;
            }
            else if(value != 0)
            {
                FuriProp += SL.FuriList[Listnum].Length;
            }
            Listnum = value;
        }
    }

    //ローマ字の設定
    public int RomaProp
    {
        get { return Romanum; }
        set
        {
            Romanum = value;

            if (Romanum == SL.RomaList[Listnum].Length)
            {
                ListProp++;
                TS.ResetTypedStr();
                RomaProp = 0;
            }
            else
            {
                var romastr = "<color=\"red\">";

                for (int i = 0; i < SL.RomaList.Count; i++)
                {
                    var str = SL.RomaList[i];

                    if (i != Listnum)
                    {
                        romastr += str;
                    }
                    else
                    {
                        romastr += str.Insert(Romanum, "</color>");
                    }
                }
                RomaText.text = romastr;
            }
        }
    }

    private void Start()
    {
        SL = GetComponent<SetList>();
        TS = GetComponent<TypingSystem>();

        SetStrList(Hiragana, HiraList);
        SetStrList(Furigana, FuriList);
        SetStrList(Kanji, KanjiList);

        ResetText();

        void SetStrList(TextAsset textAsset, List<string> list)
        {
            var array = textAsset.text.Split('\n');
            list.AddRange(array);
        }
    }

    //テキストを初期化する
    private void ResetText()
    {
        SL.FuriList.Clear();
        SL.RomaList.Clear();
        SL.KanjiFuriList.Clear();

        SetColumunNum++;
    }

}

TypingSystem.cs

txtファイルや表示中の文字列の行数、打ち終わった字数などを管理しています。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class TypingSystem : MonoBehaviour
{
    private SetList SL;
    private SetText ST;
    private Dictionary Dic;

    private string Typedstr;

    public void ResetTypedStr()
    {
        Typedstr = "";
    }

    private void Awake()
    {
        ST = GetComponent<SetText>();
        SL = GetComponent<SetList>();
        Dic = GetComponent<Dictionary>();
    }

    string[] keys = { "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", 
        "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "-", ",",".","[","]"};

    //keys内にあるキーを打った時
    void Update()
    {
        foreach (string key in keys)
        {
            if (Input.GetKeyDown(key))
            {
                TypeJudge(key);
            }
        }
    }

    public void TypeJudge(string type)
    {
        //打っている途中の文字と現在打った文字をつなげる
        var JudgeStr = Typedstr + type;
        bool jud = false;

        //打った文字が n で 前の文字が nn じゃない場合
        if (type == "n" && SL.RomaList[ST.Listnum] == "n")
        {
            SL.RomaList[ST.Listnum] = "nn";
            RightEnter();
            return;
        }

        //正しく打てている場合
        if (type == SL.RomaList[ST.Listnum][ST.RomaProp].ToString())
        {
            RightEnter();
        }
        else
        {
            //Dictionaryの中のEpicDic内の文字を打とうとしている場合
            for (int i = SL.FuriList[ST.Listnum].Length; i != 0 && !jud ; i--)
            {
                var SearchStr = SL.FuriList[ST.Listnum].Substring(0,i);

                if (i != 1 && SL.FuriList[ST.Listnum][0].ToString() == "っ")
                {
                    SearchStr = SearchStr.Substring(1, SearchStr.Length-1);
                }

                var list = Dic.SearchTotaldicKey(SearchStr);

                foreach (var d in list)
                {
                    var MatchStr = "";

                    if (i != 1 && SL.FuriList[ST.Listnum][0].ToString() == "っ")
                    {
                        MatchStr += d.Value.Substring(0, 1);
                    }

                    if (JudgeStr.Length <= MatchStr.Length + d.Value.Length && !jud)
                    {

                        MatchStr += d.Value;

                        if (JudgeStr == MatchStr.Substring(0, JudgeStr.Length))
                        {
                            //print(MatchStr + "で見つかりました");
                            TypeMatch(i , MatchStr);
                        }
                    }
                }
            }

            //該当した文字が無い場合(ミスタイプしている場合)
            if(!jud)
            {
                MissEnter();
            }

        }

        //EpicDic内にある文字が正しく打てている時に呼び出される
        //打った文字とまだ打っていない文字を切り分ける作業をする
        void TypeMatch(int num, string MatchStr)
        {
            jud = true;
            var Furistr = SL.FuriList[ST.Listnum];
            SL.FuriList[ST.Listnum] = Furistr.Substring(0, num);
            SL.RomaList[ST.Listnum] = MatchStr;

            if(Furistr.Length != num)
            {
                SL.FuriList.Insert(ST.Listnum + 1, Furistr.Substring(num));
                SL.RomaList.Insert(ST.Listnum + 1, Dic.SearchdicKey(SL.FuriList[ST.Listnum + 1]).Value);
            }

            Typedstr = JudgeStr;
            RightEnter();
        }

        //ミスタイプしたとき
        void MissEnter()
        {
            print("ミスしました!");
        }

        //正しく打てているとき
        void RightEnter()
        {
            Typedstr = JudgeStr;
            ST.Romanum++;
        }
    }
}

実際に動かしてみる

上にあるプログラムを全て同じオブジェクトにアタッチし、以下の画像のように設定すると動きます。
キャプチャ.PNG

完成した作品

そしてこのプログラムを組み込んで実際に完成した作品がこちらになります。

ケロツグ -雨乞いタイピング-

Webからのプレイはこちらから。
プロジェクトのダウンロードはこちらから

芥川龍之介の「蛙」の文をひたすら打つタイピングゲームです。
文字を打つたびに蛙が増えていき、正確なタイピングを続けると制限時間が増えたりします。

最後に

作るのは苦戦しましたが内容は結構シンプルな構造になっていると思います。
この方法で作っていると対応していないキーが出てくると思います。
見つけた場合はDisctionary.csの方に書き足して頂けると対応されると思います。

最後まで読んでいただきありがとうございました。

ツイッターでシェア
みんなに共有、忘れないようにメモ

かの

古き良き昭和の香り

Crieitは誰でも投稿できるサービスです。 是非記事の投稿をお願いします。どんな軽い内容でも投稿できます。

また、「こんな記事が読みたいけど見つからない!」という方は是非記事投稿リクエストボードへ!

有料記事を販売できるようになりました!

こじんまりと作業ログやメモ、進捗を書き残しておきたい方はボード機能をご利用ください。
ボードとは?

コメント